Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

432. All O`one Data Structure - Leetcode Solution

Problem Description

The Leetcode problem "All O`one Data Structure" asks you to design a data structure that supports the following operations in O(1) time:

  • inc(key): Increments the count of key by 1. If key does not exist, insert it with count 1.
  • dec(key): Decrements the count of key by 1. If key's count becomes 0, remove it from the data structure.
  • getMaxKey(): Returns one of the keys with the maximal count. If no keys exist, return an empty string.
  • getMinKey(): Returns one of the keys with the minimal count. If no keys exist, return an empty string.

The challenge is to ensure all operations run in constant time, even as the number of keys grows. You may return any valid key for getMaxKey and getMinKey. Keys are strings, and counts are positive integers.

Thought Process

At first glance, the problem seems simple: we can use a hash map to store each key and its count. However, the real challenge lies in supporting getMaxKey() and getMinKey() in O(1) time. If we use only a hash map, we would need to scan all keys to find the max or min count, which is O(n).

To overcome this, we need a way to track keys by their counts efficiently, and quickly update this tracking as counts change. This leads us to consider combining a hash map (for O(1) key lookups and updates) with a doubly linked list (to maintain order of counts and allow O(1) insertions/deletions).

The conceptual leap is realizing we should not track individual keys in the list, but rather group keys by their counts in "buckets" and order these buckets by count. This way, we can always access the min and max count groups in O(1) time.

Solution Approach

We construct a custom data structure using two main components:

  • Hash Map 1: keyCount maps each key to its count.
  • Hash Map 2: countBucket maps each count to a bucket node in a doubly linked list. Each bucket node contains a set of keys with that count.
  • Doubly Linked List: The list of buckets is ordered by count. The head always has the minimal count, the tail the maximal count.

The algorithm works as follows:

  1. When inc(key) is called:
    • If key is new, insert it into the count 1 bucket. If this bucket doesn't exist, create it and add to the head of the list.
    • If key exists, move it from its current count bucket to the next higher count bucket (creating the bucket if needed), and remove it from the old bucket. If the old bucket is empty, remove it from the list.
  2. When dec(key) is called:
    • If key doesn't exist, do nothing.
    • If the count becomes 0, remove key from the data structure and its bucket. If the bucket is empty, remove it.
    • Otherwise, move key to the previous lower count bucket (creating it if necessary).
  3. getMaxKey() returns any key from the tail bucket (max count), or "" if empty.
  4. getMinKey() returns any key from the head bucket (min count), or "" if empty.

The use of two hash maps and a doubly linked list ensures all operations are O(1): hash map lookups/updates are O(1), and linked list node insertions/removals are also O(1).

Example Walkthrough

Let's walk through a sequence of operations:

  1. inc("apple"): "apple" is new. Create count 1 bucket and add "apple".
  2. inc("banana"): "banana" is new. Add to existing count 1 bucket. Now, count 1 bucket has {"apple", "banana"}.
  3. inc("apple"): "apple" moves from count 1 to count 2. Remove "apple" from count 1 bucket, add to new count 2 bucket. Now, count 1 bucket has {"banana"}, count 2 bucket has {"apple"}.
  4. getMaxKey(): Returns "apple" (from count 2 bucket).
  5. getMinKey(): Returns "banana" (from count 1 bucket).
  6. dec("apple"): "apple" moves from count 2 to count 1. Remove from count 2 bucket, add to count 1 bucket. Now, count 2 bucket is empty and removed. Count 1 bucket has {"apple", "banana"}.
  7. getMaxKey(): Returns either "apple" or "banana" (both have count 1).

At each step, all operations (move, insert, delete, get) are performed in constant time.

Time and Space Complexity

Brute-force Approach: If we used only a hash map, inc and dec would be O(1), but getMaxKey and getMinKey would be O(n) because we would need to scan all keys.

Optimized Approach (Bucket List):

  • All operations (inc, dec, getMaxKey, getMinKey) are O(1) because:
    • Hash map lookups and updates are O(1).
    • Linked list insertions/deletions are O(1) with direct node references.
  • Space complexity is O(n + m), where n is the number of keys and m is the number of unique counts (buckets).

Summary

The "All O`one Data Structure" problem is a classic example of combining data structures for optimal efficiency. By grouping keys by counts in a doubly linked list of buckets and maintaining fast access with hash maps, we achieve O(1) operations for all required methods. The key insight is to avoid scanning for min/max by always keeping them at the ends of the list, and to carefully update our data structure as counts change.

This approach elegantly balances fast updates and queries, and is a powerful pattern for designing advanced data structures.

Code Implementation

class Node:
    def __init__(self, count):
        self.count = count
        self.keys = set()
        self.prev = None
        self.next = None

class AllOne:
    def __init__(self):
        self.keyCount = {}  # key -> count
        self.countBucket = {}  # count -> Node
        self.head = Node(float('-inf'))
        self.tail = Node(float('inf'))
        self.head.next = self.tail
        self.tail.prev = self.head

    def _addNodeAfter(self, newNode, prevNode):
        nxt = prevNode.next
        prevNode.next = newNode
        newNode.prev = prevNode
        newNode.next = nxt
        nxt.prev = newNode

    def _removeNode(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev
        del self.countBucket[node.count]

    def inc(self, key: str) -> None:
        count = self.keyCount.get(key, 0)
        newCount = count + 1
        self.keyCount[key] = newCount

        if newCount not in self.countBucket:
            newNode = Node(newCount)
            self.countBucket[newCount] = newNode
            if count == 0:
                self._addNodeAfter(newNode, self.head)
            else:
                self._addNodeAfter(newNode, self.countBucket[count])
        node = self.countBucket[newCount]
        node.keys.add(key)

        if count > 0:
            prevNode = self.countBucket[count]
            prevNode.keys.remove(key)
            if not prevNode.keys:
                self._removeNode(prevNode)

    def dec(self, key: str) -> None:
        if key not in self.keyCount:
            return
        count = self.keyCount[key]
        node = self.countBucket[count]
        node.keys.remove(key)

        if count == 1:
            del self.keyCount[key]
        else:
            newCount = count - 1
            self.keyCount[key] = newCount
            if newCount not in self.countBucket:
                newNode = Node(newCount)
                self.countBucket[newCount] = newNode
                self._addNodeAfter(newNode, node.prev)
            self.countBucket[newCount].keys.add(key)

        if not node.keys:
            self._removeNode(node)

    def getMaxKey(self) -> str:
        if self.tail.prev == self.head:
            return ""
        node = self.tail.prev
        return next(iter(node.keys))

    def getMinKey(self) -> str:
        if self.head.next == self.tail:
            return ""
        node = self.head.next
        return next(iter(node.keys))
      
class AllOne {
    struct Node {
        int count;
        unordered_set<string> keys;
        Node* prev;
        Node* next;
        Node(int c) : count(c), prev(nullptr), next(nullptr) {}
    };
    unordered_map<string, int> keyCount;
    unordered_map<int, Node*> countBucket;
    Node* head;
    Node* tail;
public:
    AllOne() {
        head = new Node(INT_MIN);
        tail = new Node(INT_MAX);
        head->next = tail;
        tail->prev = head;
    }
    void addNodeAfter(Node* newNode, Node* prevNode) {
        Node* nxt = prevNode->next;
        prevNode->next = newNode;
        newNode->prev = prevNode;
        newNode->next = nxt;
        nxt->prev = newNode;
    }
    void removeNode(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
        countBucket.erase(node->count);
        delete node;
    }
    void inc(string key) {
        int count = keyCount.count(key) ? keyCount[key] : 0;
        int newCount = count + 1;
        keyCount[key] = newCount;
        if (!countBucket.count(newCount)) {
            Node* newNode = new Node(newCount);
            countBucket[newCount] = newNode;
            if (count == 0)
                addNodeAfter(newNode, head);
            else
                addNodeAfter(newNode, countBucket[count]);
        }
        countBucket[newCount]->keys.insert(key);
        if (count > 0) {
            Node* prevNode = countBucket[count];
            prevNode->keys.erase(key);
            if (prevNode->keys.empty())
                removeNode(prevNode);
        }
    }
    void dec(string key) {
        if (!keyCount.count(key))
            return;
        int count = keyCount[key];
        Node* node = countBucket[count];
        node->keys.erase(key);
        if (count == 1) {
            keyCount.erase(key);
        } else {
            int newCount = count - 1;
            keyCount[key] = newCount;
            if (!countBucket.count(newCount)) {
                Node* newNode = new Node(newCount);
                countBucket[newCount] = newNode;
                addNodeAfter(newNode, node->prev);
            }
            countBucket[newCount]->keys.insert(key);
        }
        if (node->keys.empty())
            removeNode(node);
    }
    string getMaxKey() {
        if (tail->prev == head)
            return "";
        return *(tail->prev->keys.begin());
    }
    string getMinKey() {
        if (head->next == tail)
            return "";
        return *(head->next->keys.begin());
    }
};
      
class AllOne {
    private class Node {
        int count;
        Set<String> keys;
        Node prev, next;
        Node(int c) {
            count = c;
            keys = new HashSet<>();
        }
    }
    private Map<String, Integer> keyCount = new HashMap<>();
    private Map<Integer, Node> countBucket = new HashMap<>();
    private Node head, tail;

    public AllOne() {
        head = new Node(Integer.MIN_VALUE);
        tail = new Node(Integer.MAX_VALUE);
        head.next = tail;
        tail.prev = head;
    }
    private void addNodeAfter(Node newNode, Node prevNode) {
        Node nxt = prevNode.next;
        prevNode.next = newNode;
        newNode.prev = prevNode;
        newNode.next = nxt;
        nxt.prev = newNode;
    }
    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
        countBucket.remove(node.count);
    }
    public void inc(String key) {
        int count = keyCount.getOrDefault(key, 0);
        int newCount = count + 1;
        keyCount.put(key, newCount);
        if (!countBucket.containsKey(newCount)) {
            Node newNode = new Node(newCount);
            countBucket.put(newCount, newNode);
            if (count == 0)
                addNodeAfter(newNode, head);
            else
                addNodeAfter(newNode, countBucket.get(count));
        }
        countBucket.get(newCount).keys.add(key);
        if (count > 0) {
            Node prevNode = countBucket.get(count);
            prevNode.keys.remove(key);
            if (prevNode.keys.isEmpty())
                removeNode(prevNode);
        }
    }
    public void dec(String key) {
        if (!keyCount.containsKey(key))
            return;
        int count = keyCount.get(key);
        Node node = countBucket.get(count);
        node.keys.remove(key);
        if (count == 1) {
            keyCount.remove(key);
        } else {
            int newCount = count - 1;
            keyCount.put(key, newCount);
            if (!countBucket.containsKey(newCount)) {
                Node newNode = new Node(newCount);
                countBucket.put(newCount, newNode);
                addNodeAfter(newNode, node.prev);
            }
            countBucket.get(newCount).keys.add(key);
        }
        if (node.keys.isEmpty())
            removeNode(node);
    }
    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();
    }
}
      
class Node {
    constructor(count) {
        this.count = count;
        this.keys = new Set();
        this.prev = null;
        this.next = null;
    }
}

class AllOne {
    constructor() {
        this.keyCount = new Map();
        this.countBucket = new Map();
        this.head = new Node(-Infinity);
        this.tail = new Node(Infinity);
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }
    _addNodeAfter(newNode, prevNode) {
        let nxt = prevNode.next;
        prevNode.next = newNode;
        newNode.prev = prevNode;
        newNode.next = nxt;
        nxt.prev = newNode;
    }
    _removeNode(node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
        this.countBucket.delete(node.count);
    }
    inc(key) {
        let count = this.keyCount.has(key) ? this.keyCount.get(key) : 0;
        let newCount = count + 1;
        this.keyCount.set(key, newCount);
        if (!this.countBucket.has(newCount)) {
            let newNode = new Node(newCount);
            this.countBucket.set(newCount, newNode);
            if (count === 0) {
                this._addNodeAfter(newNode, this.head);
            } else {
                this._addNodeAfter(newNode, this.countBucket.get(count));
            }
        }
        let node = this.countBucket.get(newCount);
        node.keys.add(key);

        if (count > 0) {
            let prevNode = this.countBucket.get(count);
            prevNode.keys.delete(key);
            if (prevNode.keys.size === 0) {
                this._removeNode(prevNode);
            }
        }
    }
    dec(key) {
        if (!this.keyCount.has(key)) return;
        let count = this.keyCount.get(key);
        let node = this.countBucket.get(count);
        node.keys.delete(key);

        if (count === 1) {
            this.keyCount.delete(key);
        } else {
            let newCount = count - 1;
            this.keyCount.set(key, newCount);
            if (!this.countBucket.has(newCount)) {
                let newNode = new Node(newCount);
                this.countBucket.set(newCount, newNode);
                this._addNodeAfter(newNode, node.prev);
            }
            this.countBucket.get(newCount).keys.add(key);
        }
        if (node.keys.size === 0) {
            this._removeNode(node);
        }
    }
    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;
    }
}