432. All O`one Data Structure
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 string key by 1. If key does not exist in the data structure, insert it with count 1.
- dec(String key) Decrements the count of the string key by 1. If the count of key is 0 after the decrement, remove it from the data structure. It is guaranteed that key exists 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 <= 10
- key consists of lowercase English letters.
- It is guaranteed that for each call to dec, key is existing in the data structure.
- At most 5 ∗ 1 0 4 5 * 10^4 5∗104 calls will be made to inc, dec, getMaxKey, and getMinKey.
From: LeetCode
Link: 432. All O`one Data Structure
Solution:
Ideas:
1. Efficiently Managing Counts with a Linked List:
- The linked list allows constant-time insertion and deletion of nodes with specific counts. Nodes are always ordered by count, so head->next gives the minimum count, and tail->prev gives the maximum count.
2. Using Hash Tables for Fast Lookup:
- Hash tables (data and keys) allow fast lookups for O(1) access to nodes and strings.
- data provides access to each key’s count and associated Node, while keys in each Node provides direct access to strings within that count.
3. Handling Edge Cases with Sentinel Nodes:
- Sentinel nodes (head and tail) simplify boundary conditions. There’s no need to check if we’re at the start or end of the list when updating nodes for incrementing or decrementing counts.
4. Memory Management and Avoiding Dangling Pointers:
- Temporary pointers (nodeToRemove and nextNode) are used before deleting nodes to avoid heap-use-after-free errors by ensuring that no dangling references remain after node deletion.
Code:
typedef struct Node {
int count;
struct Node *prev, *next;
struct KeyNode *keys;
} Node;
typedef struct KeyNode {
char *key;
UT_hash_handle hh;
} KeyNode;
typedef struct DataEntry {
char *key;
int count;
Node *node;
UT_hash_handle hh;
} DataEntry;
typedef struct {
Node *head, *tail;
DataEntry *data;
} AllOne;
Node* newNode(int count) {
Node *node = (Node*)malloc(sizeof(Node));
node->count = count;
node->prev = node->next = NULL;
node->keys = NULL;
return node;
}
void addKey(Node *node, char *key) {
KeyNode *keyNode = (KeyNode*)malloc(sizeof(KeyNode));
keyNode->key = key;
HASH_ADD_KEYPTR(hh, node->keys, key, strlen(key), keyNode);
}
void removeKey(Node *node, char *key) {
KeyNode *keyNode;
HASH_FIND_STR(node->keys, key, keyNode);
if (keyNode) {
HASH_DEL(node->keys, keyNode);
free(keyNode);
}
}
AllOne* allOneCreate() {
AllOne *obj = (AllOne*)malloc(sizeof(AllOne));
obj->head = newNode(INT_MIN);
obj->tail = newNode(INT_MAX);
obj->head->next = obj->tail;
obj->tail->prev = obj->head;
obj->data = NULL;
return obj;
}
Node* insertNodeAfter(Node *node, int count) {
Node *newNode = (Node*)malloc(sizeof(Node));
newNode->count = count;
newNode->keys = NULL;
newNode->prev = node;
newNode->next = node->next;
node->next->prev = newNode;
node->next = newNode;
return newNode;
}
void removeNode(Node *node) {
node->prev->next = node->next;
node->next->prev = node->prev;
HASH_CLEAR(hh, node->keys);
free(node);
}
void allOneInc(AllOne* obj, char* key) {
DataEntry *entry;
HASH_FIND_STR(obj->data, key, entry);
if (!entry) {
// Key not found, add new entry with count 1
entry = (DataEntry*)malloc(sizeof(DataEntry));
entry->key = strdup(key);
entry->count = 1;
if (obj->head->next->count != 1) {
entry->node = insertNodeAfter(obj->head, 1);
} else {
entry->node = obj->head->next;
}
HASH_ADD_KEYPTR(hh, obj->data, entry->key, strlen(entry->key), entry);
addKey(entry->node, entry->key);
} else {
// Key exists, increment its count
Node *node = entry->node;
if (node->next->count != entry->count + 1) {
node = insertNodeAfter(node, entry->count + 1);
} else {
node = node->next;
}
addKey(node, key);
entry->node = node;
entry->count++;
removeKey(entry->node->prev, key);
if (entry->node->prev->keys == NULL && entry->node->prev != obj->head) {
Node *nodeToRemove = entry->node->prev;
entry->node->prev = NULL; // Avoid dangling reference in entry
removeNode(nodeToRemove);
}
}
}
void allOneDec(AllOne* obj, char* key) {
DataEntry *entry;
HASH_FIND_STR(obj->data, key, entry);
if (!entry) return; // Check if entry is NULL to avoid undefined behavior
if (entry->count == 1) {
// Remove entry if count goes to 0
Node *nodeToRemove = entry->node; // Temporary reference
removeKey(nodeToRemove, key);
HASH_DEL(obj->data, entry); // Remove entry from hash table
free(entry->key); // Free the key memory
free(entry); // Free the entry itself
if (nodeToRemove->keys == NULL && nodeToRemove != obj->head && nodeToRemove != obj->tail) {
removeNode(nodeToRemove); // Remove the node if no keys remain
}
} else {
// Decrement count
Node *node = entry->node;
if (node->prev->count != entry->count - 1) {
node = insertNodeAfter(node->prev, entry->count - 1);
} else {
node = node->prev;
}
addKey(node, key);
entry->node = node;
entry->count--;
Node *nextNode = entry->node->next; // Temporary pointer for next node
removeKey(nextNode, key);
if (nextNode->keys == NULL && nextNode != obj->tail) {
removeNode(nextNode);
}
}
}
char* allOneGetMaxKey(AllOne* obj) {
if (obj->tail->prev->keys)
return obj->tail->prev->keys->key;
return "";
}
char* allOneGetMinKey(AllOne* obj) {
if (obj->head->next->keys)
return obj->head->next->keys->key;
return "";
}
void allOneFree(AllOne* obj) {
Node *node = obj->head;
while (node) {
Node *next = node->next;
HASH_CLEAR(hh, node->keys);
free(node);
node = next;
}
DataEntry *entry, *tmp;
HASH_ITER(hh, obj->data, entry, tmp) {
HASH_DEL(obj->data, entry);
free(entry->key);
free(entry);
}
free(obj);
}