All O`one Data Structure
HardPrompt
Design a data structure to store the strings' count with the ability to return the strings with minimum and maximum counts.
Implement the AllOne class:
AllOne()Initializes the object of the data structure.inc(String key)Increments the count of the stringkeyby1. Ifkeydoes not exist in the data structure, insert it with count1.dec(String key)Decrements the count of the stringkeyby1. If the count ofkeyis0after the decrement, remove it from the data structure. It is guaranteed thatkeyexists in the data structure before the decrement.getMaxKey()Returns one of the keys with the maximal count. If no element exists, return an empty string"".getMinKey()Returns one of the keys with the minimum count. If no element exists, return an empty string"".
Note that each function must run in O(1) average time complexity.
Example 1:
Input
["AllOne", "inc", "inc", "getMaxKey", "getMinKey", "inc", "getMaxKey", "getMinKey"]
[[], ["hello"], ["hello"], [], [], ["leet"], [], []]
Output
[null, null, null, "hello", "hello", null, "hello", "leet"]
Explanation
AllOne allOne = new AllOne();
allOne.inc("hello");
allOne.inc("hello");
allOne.getMaxKey(); // return "hello"
allOne.getMinKey(); // return "hello"
allOne.inc("leet");
allOne.getMaxKey(); // return "hello"
allOne.getMinKey(); // return "leet"
Constraints:
1 <= key.length <= 10keyconsists of lowercase English letters.- It is guaranteed that for each call to
dec,keyis existing in the data structure. - At most
5 * 104calls will be made toinc,dec,getMaxKey, andgetMinKey.
Approaches
3 approaches with complexity analysis and trade-offs.
This is a straightforward brute-force approach that uses a single HashMap to store the counts of the strings. While inc and dec operations are efficient, finding the key with the minimum or maximum count requires iterating through all the keys in the map, which is too slow for the problem's constraints.
Algorithm
- Use a
HashMap<String, Integer>to store the count for each string key. inc(key): Retrieve the count of the key, increment it, and update the map. If the key doesn't exist, add it with a count of 1. This is an O(1) average time operation.dec(key): Retrieve the count of the key, decrement it, and update the map. If the new count is 0, remove the key from the map. This is an O(1) average time operation.getMaxKey()/getMinKey(): Iterate through all the entries in theHashMap. Keep track of the maximum/minimum count seen so far and the corresponding key. Return the key associated with the final maximum/minimum count. This requires a full scan of all unique keys, making it an O(N) operation, where N is the number of unique keys.
Walkthrough
The core of this approach is a HashMap<String, Integer> which maps each string key to its frequency count.
For inc(key) and dec(key), we can leverage the HashMap's O(1) average time complexity for get and put operations. To increment, we fetch the current count, add one, and put it back. To decrement, we do the same but subtract one, and if the count reaches zero, we remove the key from the map.
The main drawback appears in getMaxKey() and getMinKey(). Since the counts in the HashMap are not stored in any particular order, the only way to find the minimum or maximum count is to perform a linear scan over all the entries in the map. We iterate through the map, comparing each key's count with the current minimum/maximum found so far, and update it if necessary. This leads to an O(N) time complexity, where N is the number of unique keys stored.
import java.util.HashMap;import java.util.Map; class AllOne { private Map<String, Integer> counts; public AllOne() { counts = new HashMap<>(); } public void inc(String key) { counts.put(key, counts.getOrDefault(key, 0) + 1); } public void dec(String key) { int count = counts.get(key); if (count == 1) { counts.remove(key); } else { counts.put(key, count - 1); } } public String getMaxKey() { if (counts.isEmpty()) { return ""; } String maxKey = ""; int maxVal = 0; for (Map.Entry<String, Integer> entry : counts.entrySet()) { if (entry.getValue() > maxVal) { maxVal = entry.getValue(); maxKey = entry.getKey(); } } return maxKey; } public String getMinKey() { if (counts.isEmpty()) { return ""; } String minKey = ""; int minVal = Integer.MAX_VALUE; for (Map.Entry<String, Integer> entry : counts.entrySet()) { if (entry.getValue() < minVal) { minVal = entry.getValue(); minKey = entry.getKey(); } } return minKey; }}Complexity
Time
`inc` and `dec`: O(1) average time. `getMaxKey` and `getMinKey`: O(N) time, where N is the number of unique keys.
Space
O(N), where N is the number of unique keys stored in the HashMap.
Trade-offs
Pros
Very simple to understand and implement.
incanddecoperations are efficient (O(1) average time).
Cons
The
getMaxKey()andgetMinKey()methods are inefficient, with a time complexity of O(N).This approach does not meet the O(1) time complexity requirement for all functions.
This approach improves upon the brute-force method by using a balanced binary search tree (implemented as a TreeSet in Java) alongside a HashMap. The TreeSet keeps key-count pairs sorted by count, which makes finding the min and max keys an O(1) operation. However, the trade-off is that updating a key's count (inc and dec) now requires updating the TreeSet, which takes O(log N) time.
Algorithm
- Use a
HashMap<String, Integer>to map keys to their counts. - Use a
TreeSetto store(count, key)pairs. TheTreeSetwill automatically keep these pairs sorted based on the count, allowing for fast retrieval of min and max elements. inc(key): Get the old count from the HashMap. Remove the old(count, key)pair from theTreeSet. Update the count in the HashMap. Insert the new(count+1, key)pair into theTreeSet. This involves remove and add operations on theTreeSet, which take O(log N) time.dec(key): Similar toinc, remove the old pair from theTreeSet, update the count in the HashMap, and insert the new(count-1, key)pair if the new count is greater than 0. This also takes O(log N) time.getMaxKey(): Return the key from the last element in theTreeSet(treeSet.last()). This is an O(1) operation.getMinKey(): Return the key from the first element in theTreeSet(treeSet.first()). This is an O(1) operation.
Walkthrough
To optimize getMaxKey and getMinKey, we need a data structure that keeps track of the order of counts. A balanced binary search tree, such as Java's TreeSet, is a good candidate.
We use two data structures:
HashMap<String, Integer> keyToCount: Same as before, for O(1) lookup of a key's count.TreeSet<Pair<Integer, String>> sortedCounts: This set stores pairs of(count, key). It's ordered by count, and then by key lexicographically to handle ties and ensure uniqueness of pairs.
When inc(key) is called, we must perform the following steps:
- Look up the old count in
keyToCount. - If the key existed, remove the
(oldCount, key)pair from theTreeSet. - Increment the count and update
keyToCount. - Insert the
(newCount, key)pair into theTreeSet.
Both removal and insertion in a TreeSet take O(log N) time. The dec operation follows a similar logic. Because the TreeSet is always sorted, getMinKey can be found by looking at the first element (treeSet.first()) and getMaxKey by looking at the last element (treeSet.last()), both in O(1) time.
import java.util.HashMap;import java.util.Map;import java.util.TreeSet; class AllOne { // A helper class to store count and key, and define comparison logic. private static class Pair implements Comparable<Pair> { int count; String key; Pair(int count, String key) { this.count = count; this.key = key; } @Override public int compareTo(Pair other) { if (this.count == other.count) { return this.key.compareTo(other.key); } return Integer.compare(this.count, other.count); } } private Map<String, Integer> keyToCount; private TreeSet<Pair> sortedCounts; public AllOne() { keyToCount = new HashMap<>(); sortedCounts = new TreeSet<>(); } public void inc(String key) { int oldCount = keyToCount.getOrDefault(key, 0); int newCount = oldCount + 1; if (oldCount > 0) { sortedCounts.remove(new Pair(oldCount, key)); } keyToCount.put(key, newCount); sortedCounts.add(new Pair(newCount, key)); } public void dec(String key) { int oldCount = keyToCount.get(key); int newCount = oldCount - 1; sortedCounts.remove(new Pair(oldCount, key)); if (newCount == 0) { keyToCount.remove(key); } else { keyToCount.put(key, newCount); sortedCounts.add(new Pair(newCount, key)); } } public String getMaxKey() { if (sortedCounts.isEmpty()) { return ""; } return sortedCounts.last().key; } public String getMinKey() { if (sortedCounts.isEmpty()) { return ""; } return sortedCounts.first().key; }}Complexity
Time
`inc` and `dec`: O(log N) time. `getMaxKey` and `getMinKey`: O(1) time.
Space
O(N), where N is the number of unique keys. Both the HashMap and TreeSet store N items.
Trade-offs
Pros
Achieves O(1) time complexity for
getMaxKeyandgetMinKey.Conceptually simpler than the fully O(1) solution.
Cons
incanddecoperations have a time complexity of O(log N), which does not meet the strict O(1) requirement for all functions.
This optimal approach achieves O(1) average time complexity for all operations by combining a HashMap with a custom doubly linked list. The core idea is to group keys with the same count into 'buckets'. These buckets are nodes in a doubly linked list, sorted by count. This structure allows for moving a key from one count group to an adjacent one (e.g., from count 5 to 6) in constant time, by just updating a few pointers and hash set entries.
Algorithm
- Use a
HashMap<String, Bucket>to map each key to aBucketnode in a doubly linked list. - The doubly linked list contains
Bucketnodes. EachBucketrepresents a specific count and holds aSet<String>of all keys with that count. - The list of buckets is kept sorted by count. We use
headandtaildummy nodes to easily find the min and max buckets (head.nextandtail.prev). inc(key):- Find the key's current bucket. Let its count be
c. - Look for the bucket for count
c+1, which should becurrentBucket.next. - If
currentBucket.nextis not for countc+1, create a new bucket forc+1and insert it. - Move the key from the current bucket's set to the new bucket's set.
- Update the key's mapping in the HashMap to point to the new bucket.
- If the old bucket becomes empty, remove it from the list.
- Find the key's current bucket. Let its count be
dec(key): Works similarly toinc, but moves the key to the bucket for countc-1(theprevbucket). If the count becomes 0, the key is removed entirely. Empty buckets are cleaned up.getMaxKey(): Return any key from the set in the buckettail.prev.getMinKey(): Return any key from the set in the buckethead.next.
Walkthrough
This solution is built upon two main data structures:
HashMap<String, Bucket> keyToBucketMap: This map provides O(1) access to theBucketnode that a givenkeycurrently belongs to.- A Doubly Linked List of
Buckets: EachBucketnode in the list containsint countandSet<String> keys, where the set holds all keys that have that specific count. The list is maintained in ascending order ofcount. We use dummyheadandtailnodes to simplify list manipulations and provide O(1) access to the minimum and maximum count buckets.
How operations work:
-
inc(String key): When a key's count increases fromctoc+1, we need to move it from the bucket with countcto the bucket with countc+1. We find thec+1bucket, which must be immediately after thecbucket in our sorted list. If it doesn't exist, we create it and insert it in the correct position. Then, we move the key from the old bucket'skeysset to the new one's and update thekeyToBucketMap. If the old bucket becomes empty, we remove it from the list. All these steps—map lookups, set operations, and linked list pointer updates—are O(1) on average. -
dec(String key): This is the reverse ofinc. We move the key from bucketctoc-1. If the count drops to 0, we remove the key fromkeyToBucketMap. Again, we clean up any buckets that become empty. -
getMinKey()/getMaxKey(): Since the list is sorted by count, the bucket with the minimum count is alwayshead.next, and the one with the maximum count istail.prev. We can retrieve any key from the respective bucket'skeysset in O(1) time.
import java.util.HashMap;import java.util.HashSet;import java.util.Map;import java.util.Set; class AllOne { // Bucket node for the doubly linked list private static class Bucket { int count; Set<String> keys; Bucket prev; Bucket next; Bucket(int count) { this.count = count; this.keys = new HashSet<>(); } } private Map<String, Bucket> keyToBucketMap; private Bucket head; // Dummy head private Bucket tail; // Dummy tail public AllOne() { keyToBucketMap = new HashMap<>(); head = new Bucket(0); tail = new Bucket(0); head.next = tail; tail.prev = head; } private void removeBucket(Bucket bucket) { bucket.prev.next = bucket.next; bucket.next.prev = bucket.prev; } private void insertAfter(Bucket newBucket, Bucket prevBucket) { newBucket.prev = prevBucket; newBucket.next = prevBucket.next; prevBucket.next.prev = newBucket; prevBucket.next = newBucket; } public void inc(String key) { Bucket currentBucket = keyToBucketMap.getOrDefault(key, head); int newCount = currentBucket.count + 1; Bucket nextBucket = currentBucket.next; if (nextBucket.count != newCount) { nextBucket = new Bucket(newCount); insertAfter(nextBucket, currentBucket); } nextBucket.keys.add(key); keyToBucketMap.put(key, nextBucket); if (currentBucket != head) { currentBucket.keys.remove(key); if (currentBucket.keys.isEmpty()) { removeBucket(currentBucket); } } } public void dec(String key) { Bucket currentBucket = keyToBucketMap.get(key); currentBucket.keys.remove(key); int newCount = currentBucket.count - 1; if (newCount > 0) { Bucket prevBucket = currentBucket.prev; if (prevBucket.count != newCount) { prevBucket = new Bucket(newCount); insertAfter(prevBucket, currentBucket.prev); } prevBucket.keys.add(key); keyToBucketMap.put(key, prevBucket); } else { keyToBucketMap.remove(key); } if (currentBucket.keys.isEmpty()) { removeBucket(currentBucket); } } public String getMaxKey() { if (tail.prev == head) { return ""; } return tail.prev.keys.iterator().next(); } public String getMinKey() { if (head.next == tail) { return ""; } return head.next.keys.iterator().next(); }}Complexity
Time
`inc`, `dec`, `getMaxKey`, and `getMinKey` all have an average time complexity of O(1).
Space
O(N + C), where N is the number of unique keys and C is the number of unique counts. In the worst case, C can be equal to N, so the complexity is O(N).
Trade-offs
Pros
Achieves O(1) average time complexity for all required operations (
inc,dec,getMaxKey,getMinKey).The most efficient solution possible for the given constraints.
Cons
Significantly more complex to design and implement correctly.
Higher constant factor overhead due to managing multiple complex data structures.
Solutions
Solution
class AllOne { Node root = new Node (); Map < String , Node > nodes = new HashMap <>(); public AllOne () { root . next = root ; root . prev = root ; } public void inc ( String key ) { if (! nodes . containsKey ( key )) { if ( root . next == root || root . next . cnt > 1 ) { nodes . put ( key , root . insert ( new Node ( key , 1 ))); } else { root . next . keys . add ( key ); nodes . put ( key , root . next ); } } else { Node curr = nodes . get ( key ); Node next = curr . next ; if ( next == root || next . cnt > curr . cnt + 1 ) { nodes . put ( key , curr . insert ( new Node ( key , curr . cnt + 1 ))); } else { next . keys . add ( key ); nodes . put ( key , next ); } curr . keys . remove ( key ); if ( curr . keys . isEmpty ()) { curr . remove (); } } } public void dec ( String key ) { Node curr = nodes . get ( key ); if ( curr . cnt == 1 ) { nodes . remove ( key ); } else { Node prev = curr . prev ; if ( prev == root || prev . cnt < curr . cnt - 1 ) { nodes . put ( key , prev . insert ( new Node ( key , curr . cnt - 1 ))); } else { prev . keys . add ( key ); nodes . put ( key , prev ); } } curr . keys . remove ( key ); if ( curr . keys . isEmpty ()) { curr . remove (); } } public String getMaxKey () { return root . prev . keys . iterator (). next (); } public String getMinKey () { return root . next . keys . iterator (). next (); } } class Node { Node prev ; Node next ; int cnt ; Set < String > keys = new HashSet <>(); public Node () { this ( "" , 0 ); } public Node ( String key , int cnt ) { this . cnt = cnt ; keys . add ( key ); } public Node insert ( Node node ) { node . prev = this ; node . next = this . next ; node . prev . next = node ; node . next . prev = node ; return node ; } public void remove () { this . prev . next = this . next ; this . next . prev = this . prev ; } } /** * Your AllOne object will be instantiated and called as such: * AllOne obj = new AllOne(); * obj.inc(key); * obj.dec(key); * String param_3 = obj.getMaxKey(); * String param_4 = obj.getMinKey(); */class Node : def __init__ ( self , key = '' , cnt = 0 ): self . prev = None self . next = None self . cnt = cnt self . keys = { key } def insert ( self , node ): node . prev = self node . next = self . next node . prev . next = node node . next . prev = node return node def remove ( self ): self . prev . next = self . next self . next . prev = self . prev class AllOne : def __init__ ( self ): self . root = Node () self . root . next = self . root self . root . prev = self . root self . nodes = {} def inc ( self , key : str ) -> None : root , nodes = self . root , self . nodes if key not in nodes : if root . next == root or root . next . cnt > 1 : nodes [ key ] = root . insert ( Node ( key , 1 )) else : root . next . keys . add ( key ) nodes [ key ] = root . next else : curr = nodes [ key ] next = curr . next if next == root or next . cnt > curr . cnt + 1 : nodes [ key ] = curr . insert ( Node ( key , curr . cnt + 1 )) else : next . keys . add ( key ) nodes [ key ] = next curr . keys . discard ( key ) if not curr . keys : curr . remove () def dec ( self , key : str ) -> None : root , nodes = self . root , self . nodes curr = nodes [ key ] if curr . cnt == 1 : nodes . pop ( key ) else : prev = curr . prev if prev == root or prev . cnt < curr . cnt - 1 : nodes [ key ] = prev . insert ( Node ( key , curr . cnt - 1 )) else : prev . keys . add ( key ) nodes [ key ] = prev curr . keys . discard ( key ) if not curr . keys : curr . remove () def getMaxKey ( self ) -> str : return next ( iter ( self . root . prev . keys )) def getMinKey ( self ) -> str : return next ( iter ( self . root . next . keys )) # Your AllOne object will be instantiated and called as such: # obj = AllOne() # obj.inc(key) # obj.dec(key) # param_3 = obj.getMaxKey() # param_4 = obj.getMinKey()Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.