Blind Plus 100 Visualized

#100 ยท Design

All O(1) Data Structure

Design a data structure that stores strings with counts and supports inc(key), dec(key) (removing the key when its count reaches 0), getMaxKey() and getMinKey(), each in O(1) average time.

HardLeetCode
  • Current node
  • Link just rewired
  • Done / in result
  • Marked
  • Rejected
  • Dummy head
step 1 / 1

Pointers

Variables

Hash map / table

Stack / queue

Foundations & data-structure selection

Hash map: key โ†’ count (or key โ†’ bucket)

Constant-time access to the current count of a key. In the bucket design it maps a key straight to the bucket it lives in.

Memory layout

A hash table with one entry per stored key.

OperationCost
lookup / updateO(1) average

Sorted doubly linked list of buckets

A bucket holds all keys sharing one count. Buckets are linked in increasing order of count, so the minimum is next to the head and the maximum next to the tail. Since inc and dec change a count by exactly 1, a key only ever moves to the neighbouring bucket (or to a new bucket inserted right there): O(1).

Memory layout

Bucket nodes with count, a set of keys, prev and next; dummy head and tail bracket them. Array-backed slots with a free list avoid per-bucket allocation.

OperationCost
move a key / create / unlink a bucketO(1)
min / maxO(1)

Set of keys inside a bucket

A hash set (any element is a valid answer) so that adding and removing a key is O(1) and a representative key is available without scanning.

Memory layout

One hash set per bucket; total size equals the number of keys.

OperationCost
add / delete / any elementO(1)

Why it fits

  • min and max need an ORDER on counts, but every update changes a count by exactly one, so the sorted order only changes locally: that is what a linked list can update in O(1) while a heap or tree would need O(log n).
  • Grouping keys of equal count into one bucket keeps the list as short as the number of distinct counts and makes "any key with the max count" immediate.
  • Dummy head and tail buckets remove all edge cases: every real bucket has a neighbour on both sides.

Why alternatives degrade

Only a count map:
inc and dec are O(1) but min and max need a full scan: O(n).
Max-heap and min-heap of counts:
Updating a key's position costs O(log n) and arbitrary deletion needs extra indexing.
Balanced tree keyed by count:
Correct, but each inc / dec is O(log n), not O(1).

Four approaches, from naive to production

1 ยท beginner

Count map, scan for min / max

time inc / dec O(1), getMax / getMin O(n)space O(n)

Store only key โ†’ count. inc and dec update one entry; getMaxKey and getMinKey scan every entry.

class AllOne {
    unordered_map<string, int> counts;
public:
    void inc(string key) {
        counts[key] = counts[key] + 1;
    }
    void dec(string key) {
        auto it = counts.find(key);
        if (it == counts.end()) return;
        if (it->second == 1) counts.erase(it);
        else it->second -= 1;
    }
    string getMaxKey() {
        string best = "";
        int bestCount = 0;
        for (auto& [k, c] : counts) {
            if (c > bestCount) { best = k; bestCount = c; }
        }
        return best;
    }
    string getMinKey() {
        string best = "";
        int bestCount = INT_MAX;
        for (auto& [k, c] : counts) {
            if (c < bestCount) { best = k; bestCount = c; }
        }
        return best;
    }
};

Flaws & bottlenecks

  • getMaxKey and getMinKey are linear in the number of keys.
  • Repeated queries repeat the full scan.
  • Fails the "all O(1)" requirement.

Interviewer cross-questions

Is any maximal key acceptable?

Say: Yes: the problem allows any key with the highest (or lowest) count when several tie.

What happens when a count reaches 0?

Say: The key must be removed, otherwise getMinKey could return a key that is no longer stored.

How do you avoid the scan?

Say: Keep keys grouped by count, in sorted order of counts.

2 ยท intermediate

Count โ†’ set of keys, scan the distinct counts

time inc / dec O(1), getMax / getMin O(distinct counts)space O(n)

Add buckets: count โ†’ set of keys, with a move() helper that shifts a key from one bucket to the next and deletes emptied buckets. Max and min scan the bucket counts (not the keys).

class AllOne {
    unordered_map<string, int> counts;
    unordered_map<int, set<string>> buckets;
    void move(const string& key, int from, int to) {
        if (from > 0) {
            set<string>& old = buckets[from];
            old.erase(key);
            if (old.empty()) buckets.erase(from);
        }
        if (to > 0) {
            if (!buckets.count(to)) buckets[to] = {};
            buckets[to].insert(key);
            counts[key] = to;
        } else {
            counts.erase(key);
        }
    }
public:
    void inc(string key) {
        int c = counts.count(key) ? counts[key] : 0;
        move(key, c, c + 1);
    }
    void dec(string key) {
        auto it = counts.find(key);
        if (it == counts.end()) return;
        int c = it->second; move(key, c, c - 1);
    }
    string getMaxKey() {
        if (buckets.empty()) return "";
        int top = 0; for (auto& [c, keys] : buckets) top = max(top, c);
        return *buckets[top].begin();
    }
    string getMinKey() {
        if (buckets.empty()) return "";
        int low = INT_MAX; for (auto& [c, keys] : buckets) low = min(low, c);
        return *buckets[low].begin();
    }
};

Flaws & bottlenecks

  • The bucket map is not ordered, so min and max still scan (Math.max over all bucket counts).
  • In the worst case the number of distinct counts is about โˆš(2ยทtotal increments), not constant.
  • Spread of Math.max(...keys) can hit the argument limit for huge maps.

Interviewer cross-questions

Why is the scan much cheaper than before?

Say: It looks at distinct counts, not at keys; many keys sharing one count cost one step.

How do you make the extremes available in O(1)?

Say: Keep the buckets in a linked list sorted by count, so min and max sit at the two ends.

Why delete an emptied bucket?

Say: An empty bucket would be returned as the minimum or maximum and has no key to give back.

3 ยท advanced

Sorted doubly linked list of buckets

time O(1) per operationspace O(n)

Buckets form a doubly linked list sorted by count between dummy head and tail. inc moves the key to the next bucket (creating it right after the current one if its count is not cur + 1); dec moves it to the previous one; emptied buckets are unlinked. min is head.next, max is tail.prev.

struct Bucket {
    set<string> keys;
    Bucket *prev = nullptr, *next = nullptr;
    int count;
    Bucket(int c) : count(c) {}
};
class AllOne {
    unordered_map<string, Bucket*> nodeOf;
    Bucket* head = new Bucket(0);
    Bucket* tail = new Bucket(INT_MAX);
    Bucket* insertAfter(Bucket* node, int count) {
        Bucket* b = new Bucket(count);
        b->prev = node;
        b->next = node->next;
        node->next->prev = b;
        node->next = b;
        return b;
    }
    void unlink(Bucket* b) {
        b->prev->next = b->next;
        b->next->prev = b->prev;
        delete b;
    }
public:
    AllOne() {
        head->next = tail;
        tail->prev = head;
    }
    void inc(string key) {
        auto it = nodeOf.find(key);
        Bucket* cur = it == nodeOf.end() ? head : it->second;
        Bucket* next = cur->next;
        if (next->count != cur->count + 1) next = insertAfter(cur, cur->count + 1);
        next->keys.insert(key);
        nodeOf[key] = next;
        if (cur != head) {
            cur->keys.erase(key);
            if (cur->keys.empty()) unlink(cur);
        }
    }
    void dec(string key) {
        auto it = nodeOf.find(key);
        Bucket* cur = it == nodeOf.end() ? nullptr : it->second;
        if (!cur) return;
        if (cur->count == 1) {
            nodeOf.erase(key);
        } else {
            Bucket* prev = cur->prev;
            if (prev->count != cur->count - 1) prev = insertAfter(prev, cur->count - 1);
            prev->keys.insert(key);
            nodeOf[key] = prev;
        }
        cur->keys.erase(key);
        if (cur->keys.empty()) unlink(cur);
    }
    string getMaxKey() {
        Bucket* b = tail->prev;
        return b == head ? "" : *b->keys.begin();
    }
    string getMinKey() {
        Bucket* b = head->next;
        return b == tail ? "" : *b->keys.begin();
    }
};

Flaws & bottlenecks

  • A heap-allocated node and a hash set per bucket: allocation churn when counts change often.
  • Several pointer updates per operation (easy to get wrong).
  • Returning "any" key of the bucket relies on set iteration order being cheap.

Interviewer cross-questions

Why only look at the neighbouring bucket?

Say: A count changes by exactly 1, so the target count is cur ยฑ 1: it is either the neighbouring bucket or a bucket that must be created right next to the current one.

Why dummy head and tail with counts 0 and โˆž?

Say: They let the neighbour test (next.count !== cur.count + 1) and the unlink code run without null checks; a new key starts from the head as if it had count 0.

What about dec on an unknown key?

Say: It is a no-op here; the problem guarantees the key exists, but defensive code should not corrupt state.

4 ยท expert

Array-backed bucket slots with a free list

time O(1) per operationspace O(n)

Keep the same list but store buckets as slots in parallel arrays (count, prev, next, member set) addressed by index, and recycle emptied slots through a free stack so steady-state operation allocates no list nodes.

class AllOne {
    unordered_map<string, int> slotOf;
    vector<set<string>> members;
    vector<long long> cnt;
    vector<int> prv;
    vector<int> nxt;
    vector<int> freeSlots;
    int head, tail;
    int alloc(long long count) {
        int s;
        if (!freeSlots.empty()) { s = freeSlots.back(); freeSlots.pop_back(); }
        else { s = cnt.size(); cnt.push_back(0); members.emplace_back(); nxt.push_back(-1); prv.push_back(-1); }
        cnt[s] = count;
        members[s].clear();
        nxt[s] = -1;
        prv[s] = -1;
        return s;
    }
    void linkAfter(int s, int after) {
        nxt[s] = nxt[after];
        prv[s] = after;
        prv[nxt[after]] = s;
        nxt[after] = s;
    }
    void release(int s) {
        nxt[prv[s]] = nxt[s];
        prv[nxt[s]] = prv[s];
        freeSlots.push_back(s);
    }
public:
    AllOne() {
        head = alloc(0);
        tail = alloc(LLONG_MAX);
        nxt[head] = tail;
        prv[tail] = head;
    }
    void inc(string key) {
        auto it = slotOf.find(key);
        int cur = it == slotOf.end() ? head : it->second;
        int next = nxt[cur];
        if (cnt[next] != cnt[cur] + 1) {
            next = alloc(cnt[cur] + 1);
            linkAfter(next, cur);
        }
        members[next].insert(key);
        slotOf[key] = next;
        if (cur != head) {
            members[cur].erase(key);
            if (members[cur].empty()) release(cur);
        }
    }
    void dec(string key) {
        auto it = slotOf.find(key);
        int cur = it == slotOf.end() ? -1 : it->second;
        if (cur == -1) return;
        if (cnt[cur] == 1) {
            slotOf.erase(key);
        } else {
            int prev = prv[cur];
            if (cnt[prev] != cnt[cur] - 1) {
                prev = alloc(cnt[cur] - 1);
                linkAfter(prev, prv[cur]);
            }
            members[prev].insert(key);
            slotOf[key] = prev;
        }
        members[cur].erase(key);
        if (members[cur].empty()) release(cur);
    }
    string getMaxKey() {
        int b = prv[tail];
        return b == head ? "" : *members[b].begin();
    }
    string getMinKey() {
        int b = nxt[head];
        return b == tail ? "" : *members[b].begin();
    }
};

Flaws & bottlenecks

  • More code than the node version (alloc, linkAfter, release).
  • Each bucket still owns a hash set; a fully allocation-free version would need intrusive key lists.
  • Slot indices must never be used after release.

Interviewer cross-questions

Why recycle slots?

Say: Counts move constantly; creating and destroying bucket objects would stress the allocator. A free stack reuses the same slots.

What are the edge cases?

Say: Empty structure (head and tail adjacent: return the empty string), dec of the only key (the bucket must be unlinked), min equals max (a single bucket), and re-adding a key after it was removed.

How would you make it thread safe?

Say: Guard all four operations with one lock (queries read the list while updates relink it), or use a reader-writer lock if queries dominate.

Synchronized code walkthrough

Showing: Count map, scan for min / max โ€” shares the player above; click any step to jump.

class AllOne {
    unordered_map<string, int> counts;
public:
    void inc(string key) {
        counts[key] = counts[key] + 1;
    }
    void dec(string key) {
        auto it = counts.find(key);
        if (it == counts.end()) return;
        if (it->second == 1) counts.erase(it);
        else it->second -= 1;
    }
    string getMaxKey() {
        string best = "";
        int bestCount = 0;
        for (auto& [k, c] : counts) {
            if (c > bestCount) { best = k; bestCount = c; }
        }
        return best;
    }
    string getMinKey() {
        string best = "";
        int bestCount = INT_MAX;
        for (auto& [k, c] : counts) {
            if (c < bestCount) { best = k; bestCount = c; }
        }
        return best;
    }
};