🧠
Optimized Doubly Linked List with Hash Maps and Lazy Bucket Creation
💡 This approach refines the previous by carefully managing bucket creation and deletion to minimize overhead and memory usage, ensuring clean and efficient code.
Intuition
Use the same bucket list structure but optimize bucket insertion and removal by lazy creation and immediate cleanup, reducing unnecessary bucket nodes.
Algorithm
- Maintain doubly linked list of buckets with counts and sets of keys.
- On inc/dec, move keys between buckets, creating new buckets only when necessary.
- Remove buckets immediately when they become empty to keep list minimal.
- Use hash maps to track keys to buckets and buckets by count for quick access.
- Retrieve max/min keys from tail/head buckets respectively.
💡 The key is to keep the bucket list minimal and operations constant time by careful bucket management.
class Bucket:
def __init__(self, count):
self.count = count
self.keys = set()
self.prev = None
self.next = None
class AllOne:
def __init__(self):
self.head = Bucket(float('-inf'))
self.tail = Bucket(float('inf'))
self.head.next = self.tail
self.tail.prev = self.head
self.key_to_bucket = {}
self.count_to_bucket = {}
def _insert_bucket_after(self, new_bucket, prev_bucket):
new_bucket.prev = prev_bucket
new_bucket.next = prev_bucket.next
prev_bucket.next.prev = new_bucket
prev_bucket.next = new_bucket
self.count_to_bucket[new_bucket.count] = new_bucket
def _remove_bucket(self, bucket):
bucket.prev.next = bucket.next
bucket.next.prev = bucket.prev
del self.count_to_bucket[bucket.count]
def inc(self, key: str) -> None:
if key not in self.key_to_bucket:
if 1 not in self.count_to_bucket:
new_bucket = Bucket(1)
self._insert_bucket_after(new_bucket, self.head)
self.count_to_bucket[1].keys.add(key)
self.key_to_bucket[key] = self.count_to_bucket[1]
else:
curr_bucket = self.key_to_bucket[key]
curr_count = curr_bucket.count
next_count = curr_count + 1
if next_count not in self.count_to_bucket:
new_bucket = Bucket(next_count)
self._insert_bucket_after(new_bucket, curr_bucket)
next_bucket = self.count_to_bucket[next_count]
next_bucket.keys.add(key)
self.key_to_bucket[key] = next_bucket
curr_bucket.keys.remove(key)
if len(curr_bucket.keys) == 0:
self._remove_bucket(curr_bucket)
def dec(self, key: str) -> None:
if key not in self.key_to_bucket:
return
curr_bucket = self.key_to_bucket[key]
curr_count = curr_bucket.count
if curr_count == 1:
del self.key_to_bucket[key]
curr_bucket.keys.remove(key)
if len(curr_bucket.keys) == 0:
self._remove_bucket(curr_bucket)
else:
prev_count = curr_count - 1
if prev_count not in self.count_to_bucket:
new_bucket = Bucket(prev_count)
self._insert_bucket_after(new_bucket, curr_bucket.prev)
prev_bucket = self.count_to_bucket[prev_count]
prev_bucket.keys.add(key)
self.key_to_bucket[key] = prev_bucket
curr_bucket.keys.remove(key)
if len(curr_bucket.keys) == 0:
self._remove_bucket(curr_bucket)
def getMaxKey(self) -> str:
if self.tail.prev == self.head:
return ""
return next(iter(self.tail.prev.keys))
def getMinKey(self) -> str:
if self.head.next == self.tail:
return ""
return next(iter(self.head.next.keys))
# Driver code
if __name__ == "__main__":
obj = AllOne()
obj.inc("hello")
obj.inc("hello")
print(obj.getMaxKey()) # hello
print(obj.getMinKey()) # hello
obj.dec("hello")
print(obj.getMaxKey()) # hello
print(obj.getMinKey()) # hello
Line Notes
self.count_to_bucket = {}Map counts to bucket nodes for O(1) bucket access and quick lookup
if 1 not in self.count_to_bucketCreate bucket for count 1 only if it doesn't exist to avoid duplicates
self.count_to_bucket[next_count].keys.add(key)Add key to next count bucket after increment
del self.count_to_bucket[bucket.count]Remove bucket from map when bucket is removed to keep map consistent
self.key_to_bucket[key] = next_bucketUpdate key's bucket after increment or decrement for O(1) access
if len(curr_bucket.keys) == 0: self._remove_bucket(curr_bucket)Remove empty buckets immediately to keep list minimal
import java.util.*;
class AllOne {
private class Bucket {
int count;
Set<String> keys;
Bucket prev, next;
Bucket(int count) {
this.count = count;
keys = new HashSet<>();
}
}
private Bucket head, tail;
private Map<String, Bucket> keyToBucket;
private Map<Integer, Bucket> countToBucket;
public AllOne() {
head = new Bucket(Integer.MIN_VALUE);
tail = new Bucket(Integer.MAX_VALUE);
head.next = tail;
tail.prev = head;
keyToBucket = new HashMap<>();
countToBucket = new HashMap<>();
}
private void insertBucketAfter(Bucket newBucket, Bucket prevBucket) {
newBucket.prev = prevBucket;
newBucket.next = prevBucket.next;
prevBucket.next.prev = newBucket;
prevBucket.next = newBucket;
countToBucket.put(newBucket.count, newBucket);
}
private void removeBucket(Bucket bucket) {
bucket.prev.next = bucket.next;
bucket.next.prev = bucket.prev;
countToBucket.remove(bucket.count);
}
public void inc(String key) {
if (!keyToBucket.containsKey(key)) {
if (!countToBucket.containsKey(1)) {
Bucket newBucket = new Bucket(1);
insertBucketAfter(newBucket, head);
}
countToBucket.get(1).keys.add(key);
keyToBucket.put(key, countToBucket.get(1));
} else {
Bucket currBucket = keyToBucket.get(key);
int currCount = currBucket.count;
int nextCount = currCount + 1;
if (!countToBucket.containsKey(nextCount)) {
Bucket newBucket = new Bucket(nextCount);
insertBucketAfter(newBucket, currBucket);
}
Bucket nextBucket = countToBucket.get(nextCount);
nextBucket.keys.add(key);
keyToBucket.put(key, nextBucket);
currBucket.keys.remove(key);
if (currBucket.keys.isEmpty()) {
removeBucket(currBucket);
}
}
}
public void dec(String key) {
if (!keyToBucket.containsKey(key)) return;
Bucket currBucket = keyToBucket.get(key);
int currCount = currBucket.count;
if (currCount == 1) {
keyToBucket.remove(key);
currBucket.keys.remove(key);
if (currBucket.keys.isEmpty()) {
removeBucket(currBucket);
}
} else {
int prevCount = currCount - 1;
if (!countToBucket.containsKey(prevCount)) {
Bucket newBucket = new Bucket(prevCount);
insertBucketAfter(newBucket, currBucket.prev);
}
Bucket prevBucket = countToBucket.get(prevCount);
prevBucket.keys.add(key);
keyToBucket.put(key, prevBucket);
currBucket.keys.remove(key);
if (currBucket.keys.isEmpty()) {
removeBucket(currBucket);
}
}
}
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();
}
public static void main(String[] args) {
AllOne obj = new AllOne();
obj.inc("hello");
obj.inc("hello");
System.out.println(obj.getMaxKey()); // hello
System.out.println(obj.getMinKey()); // hello
obj.dec("hello");
System.out.println(obj.getMaxKey()); // hello
System.out.println(obj.getMinKey()); // hello
}
}
Line Notes
private Map<Integer, Bucket> countToBucket;Map counts to bucket nodes for O(1) bucket access and quick lookup
if (!countToBucket.containsKey(1))Create bucket for count 1 only if it doesn't exist to avoid duplicates
countToBucket.get(nextCount).keys.add(key);Add key to next count bucket after increment
countToBucket.remove(bucket.count);Remove bucket from map when bucket is removed to keep map consistent
keyToBucket.put(key, nextBucket);Update key's bucket after increment or decrement for O(1) access
if (currBucket.keys.isEmpty()) removeBucket(currBucket);Remove empty buckets immediately to keep list minimal
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <string>
using namespace std;
class AllOne {
struct Bucket {
int count;
unordered_set<string> keys;
Bucket* prev;
Bucket* next;
Bucket(int c) : count(c), prev(nullptr), next(nullptr) {}
};
Bucket* head;
Bucket* tail;
unordered_map<string, Bucket*> keyToBucket;
unordered_map<int, Bucket*> countToBucket;
void insertBucketAfter(Bucket* newBucket, Bucket* prevBucket) {
newBucket->prev = prevBucket;
newBucket->next = prevBucket->next;
prevBucket->next->prev = newBucket;
prevBucket->next = newBucket;
countToBucket[newBucket->count] = newBucket;
}
void removeBucket(Bucket* bucket) {
bucket->prev->next = bucket->next;
bucket->next->prev = bucket->prev;
countToBucket.erase(bucket->count);
delete bucket;
}
public:
AllOne() {
head = new Bucket(INT_MIN);
tail = new Bucket(INT_MAX);
head->next = tail;
tail->prev = head;
}
void inc(string key) {
if (keyToBucket.find(key) == keyToBucket.end()) {
if (countToBucket.find(1) == countToBucket.end()) {
Bucket* newBucket = new Bucket(1);
insertBucketAfter(newBucket, head);
}
countToBucket[1]->keys.insert(key);
keyToBucket[key] = countToBucket[1];
} else {
Bucket* currBucket = keyToBucket[key];
int currCount = currBucket->count;
int nextCount = currCount + 1;
if (countToBucket.find(nextCount) == countToBucket.end()) {
Bucket* newBucket = new Bucket(nextCount);
insertBucketAfter(newBucket, currBucket);
}
Bucket* nextBucket = countToBucket[nextCount];
nextBucket->keys.insert(key);
keyToBucket[key] = nextBucket;
currBucket->keys.erase(key);
if (currBucket->keys.empty()) {
removeBucket(currBucket);
}
}
}
void dec(string key) {
if (keyToBucket.find(key) == keyToBucket.end()) return;
Bucket* currBucket = keyToBucket[key];
int currCount = currBucket->count;
if (currCount == 1) {
keyToBucket.erase(key);
currBucket->keys.erase(key);
if (currBucket->keys.empty()) {
removeBucket(currBucket);
}
} else {
int prevCount = currCount - 1;
if (countToBucket.find(prevCount) == countToBucket.end()) {
Bucket* newBucket = new Bucket(prevCount);
insertBucketAfter(newBucket, currBucket->prev);
}
Bucket* prevBucket = countToBucket[prevCount];
prevBucket->keys.insert(key);
keyToBucket[key] = prevBucket;
currBucket->keys.erase(key);
if (currBucket->keys.empty()) {
removeBucket(currBucket);
}
}
}
string getMaxKey() {
if (tail->prev == head) return "";
return *(tail->prev->keys.begin());
}
string getMinKey() {
if (head->next == tail) return "";
return *(head->next->keys.begin());
}
};
int main() {
AllOne obj;
obj.inc("hello");
obj.inc("hello");
cout << obj.getMaxKey() << endl; // hello
cout << obj.getMinKey() << endl; // hello
obj.dec("hello");
cout << obj.getMaxKey() << endl; // hello
cout << obj.getMinKey() << endl; // hello
return 0;
}
Line Notes
unordered_map<int, Bucket*> countToBucket;Map counts to bucket nodes for O(1) bucket access and quick lookup
if (countToBucket.find(1) == countToBucket.end())Create bucket for count 1 only if it doesn't exist to avoid duplicates
countToBucket[nextCount]->keys.insert(key);Add key to next count bucket after increment
countToBucket.erase(bucket->count);Remove bucket from map when bucket is removed to keep map consistent
keyToBucket[key] = nextBucket;Update key's bucket after increment or decrement for O(1) access
if (currBucket->keys.empty()) removeBucket(currBucket);Remove empty buckets immediately to keep list minimal
class Bucket {
constructor(count) {
this.count = count;
this.keys = new Set();
this.prev = null;
this.next = null;
}
}
class AllOne {
constructor() {
this.head = new Bucket(-Infinity);
this.tail = new Bucket(Infinity);
this.head.next = this.tail;
this.tail.prev = this.head;
this.keyToBucket = new Map();
this.countToBucket = new Map();
}
_insertBucketAfter(newBucket, prevBucket) {
newBucket.prev = prevBucket;
newBucket.next = prevBucket.next;
prevBucket.next.prev = newBucket;
prevBucket.next = newBucket;
this.countToBucket.set(newBucket.count, newBucket);
}
_removeBucket(bucket) {
bucket.prev.next = bucket.next;
bucket.next.prev = bucket.prev;
this.countToBucket.delete(bucket.count);
}
inc(key) {
if (!this.keyToBucket.has(key)) {
if (!this.countToBucket.has(1)) {
const newBucket = new Bucket(1);
this._insertBucketAfter(newBucket, this.head);
}
this.countToBucket.get(1).keys.add(key);
this.keyToBucket.set(key, this.countToBucket.get(1));
} else {
const currBucket = this.keyToBucket.get(key);
const currCount = currBucket.count;
const nextCount = currCount + 1;
if (!this.countToBucket.has(nextCount)) {
const newBucket = new Bucket(nextCount);
this._insertBucketAfter(newBucket, currBucket);
}
const nextBucket = this.countToBucket.get(nextCount);
nextBucket.keys.add(key);
this.keyToBucket.set(key, nextBucket);
currBucket.keys.delete(key);
if (currBucket.keys.size === 0) {
this._removeBucket(currBucket);
}
}
}
dec(key) {
if (!this.keyToBucket.has(key)) return;
const currBucket = this.keyToBucket.get(key);
const currCount = currBucket.count;
if (currCount === 1) {
this.keyToBucket.delete(key);
currBucket.keys.delete(key);
if (currBucket.keys.size === 0) {
this._removeBucket(currBucket);
}
} else {
const prevCount = currCount - 1;
if (!this.countToBucket.has(prevCount)) {
const newBucket = new Bucket(prevCount);
this._insertBucketAfter(newBucket, currBucket.prev);
}
const prevBucket = this.countToBucket.get(prevCount);
prevBucket.keys.add(key);
this.keyToBucket.set(key, prevBucket);
currBucket.keys.delete(key);
if (currBucket.keys.size === 0) {
this._removeBucket(currBucket);
}
}
}
getMaxKey() {
if (this.tail.prev === this.head) return "";
return this.tail.prev.keys.values().next().value;
}
getMinKey() {
if (this.head.next === this.tail) return "";
return this.head.next.keys.values().next().value;
}
}
// Test
const obj = new AllOne();
obj.inc("hello");
obj.inc("hello");
console.log(obj.getMaxKey()); // hello
console.log(obj.getMinKey()); // hello
obj.dec("hello");
console.log(obj.getMaxKey()); // hello
console.log(obj.getMinKey()); // hello
Line Notes
this.countToBucket = new Map();Map counts to bucket nodes for O(1) bucket access and quick lookup
if (!this.countToBucket.has(1))Create bucket for count 1 only if it doesn't exist to avoid duplicates
this.countToBucket.get(nextCount).keys.add(key);Add key to next count bucket after increment
this.countToBucket.delete(bucket.count);Remove bucket from map when bucket is removed to keep map consistent
this.keyToBucket.set(key, nextBucket);Update key's bucket after increment or decrement for O(1) access
if (currBucket.keys.size === 0) this._removeBucket(currBucket);Remove empty buckets immediately to keep list minimal
TimeO(1) for all operations
SpaceO(n) for storing keys and buckets
By maintaining maps for counts to buckets and keys to buckets, all operations remain constant time with minimal overhead.
💡 This approach is the cleanest and most memory efficient way to implement the All O(1) Data Structure.
Interview Verdict: Accepted / Optimal and clean
This is the recommended approach to implement in interviews for clarity and efficiency.