AlgoMaster Logo

All O'one Data Structure

hardFrequencyUpdated September 21, 2026

Understanding the Problem

We need to build a data structure that tracks how many times each string has been incremented and, at any moment, can return a string with the highest count and one with the lowest, all in O(1) time.

A hash map covers the count updates: incrementing and decrementing are constant-time map operations. Finding the min and max in O(1) is the hard part. A heap gives O(log n) insert and delete. A balanced tree needs O(log n) for rebalancing. Neither meets the requirement.

Full sorting is unnecessary, though. Only the minimum and maximum matter, and a count changes by exactly 1 per operation. When a key's count goes from c to c+1, the key moves to the neighboring position in count order; nothing else shifts. That property is what makes O(1) possible.

Key Constraints:

  • At most 5 * 10^4 calls -- The cost that matters is per call. The problem requires O(1) average time per operation, which rules out heaps and balanced trees at O(log n).
  • key consists of lowercase English letters -- Keys are regular strings, no special handling needed.
  • dec is only called on existing keys -- We don't need to handle decrementing a non-existent key.

Approach 1: Brute Force with Hash Map

Intuition

Store each key's count in a hash map. Incrementing and decrementing are O(1) map updates. For getMaxKey and getMinKey, scan all entries and return the key with the highest or lowest count.

This is correct, but every getMaxKey or getMinKey call costs O(n), which violates the O(1) requirement. It still serves as a baseline: the map already answers count updates in O(1), so the min/max query is the only operation left to fix.

Algorithm

  1. Maintain a hash map counts that maps each key to its integer count.
  2. For inc(key): increment counts[key] by 1. If the key doesn't exist, set it to 1.
  3. For dec(key): decrement counts[key] by 1. If it reaches 0, remove the key.
  4. For getMaxKey(): iterate through all entries in the map and return the key with the highest count. Return "" if the map is empty.
  5. For getMinKey(): iterate through all entries and return the key with the lowest count. Return "" if the map is empty.

Example Walkthrough

Input:

0
AllOne
1
inc
2
inc
3
getMaxKey
4
getMinKey
5
inc
6
getMaxKey
7
getMinKey
operations

After inc("hello"), inc("hello"), the map holds one entry:

hello
:
2
counts

getMaxKey() scans the map and finds "hello" with count 2. getMinKey() also returns "hello" since it is the only key.

After inc("leet"), the map gains a second entry:

hello
:
2
leet
:
1
counts

getMaxKey() scans and returns "hello" (count 2). getMinKey() scans and returns "leet" (count 1). The returned sequence matches the expected output: [null, null, null, "hello", "hello", null, "hello", "leet"]. Each scan visited every entry, which is the cost the next approach removes.

Code

Every call to getMaxKey or getMinKey scans the entire hash map, recomputing the answer from scratch even though the last inc or dec changed one key's count by 1. The next approach keeps keys grouped by count in sorted order, so the current min and max always sit at the two ends of a list.

Approach 2: Doubly-Linked List of Count Buckets (Optimal)

Intuition

A count moves from c to c+1 or from c to c-1, never anywhere else. A fully sorted structure is overkill; a key only ever needs to move between adjacent "count buckets."

The structure is a doubly-linked list where each node represents one count value and holds the set of keys that currently have that count. The list stays sorted by count, smallest at the head and largest at the tail. getMinKey reads the node next to the head, getMaxKey reads the node next to the tail, both O(1).

Incrementing a key from count c to c+1 removes it from bucket c and adds it to bucket c+1. If bucket c+1 does not exist, we create a new node right after bucket c, an O(1) insertion in a doubly-linked list. If bucket c becomes empty after the removal, we splice that node out, also O(1).

A hash map from each key to its bucket node locates the key's current bucket without searching. Every operation is then a hash map lookup plus an O(1) list splice plus an O(1) set update.

Algorithm

  1. Create a doubly-linked list with sentinel head and tail nodes (count 0 and infinity). This avoids null checks.
  2. Maintain a hash map keyToNode that maps each key to its current bucket node in the linked list.
  3. For inc(key):
    • If the key is new, check if the node right after the head has count 1. If so, add the key to that node's set. Otherwise, create a new node with count 1 and insert it right after the head.
    • If the key exists in node curr (count c), move it to a node with count c+1. If the next node has count c+1, add the key there. Otherwise, create a new node with count c+1 and insert it right after curr.
    • Remove the key from its old node. If that node's set becomes empty, remove it from the linked list.
  4. For dec(key):
    • Find the key's current node curr (count c).
    • If c == 1, remove the key entirely (from both the node and the hash map).
    • Otherwise, move it to a node with count c-1. If the previous node has count c-1, add the key there. Otherwise, create a new node with count c-1 and insert it right before curr.
    • Remove the key from its old node. If that node becomes empty, remove it from the linked list.
  5. For getMaxKey(): return any key from the node right before the tail sentinel.
  6. For getMinKey(): return any key from the node right after the head sentinel.

Example Walkthrough

The trace starts with the operations from Example 1: two increments of "hello", min/max queries, an increment of "leet", and two more queries. Two dec("hello") calls follow, showing the two decrement cases: moving a key into an existing neighbor bucket and removing a key whose count reaches 0.

1Initial: empty bucket list with sentinel nodes
HEAD(0)
sentinel
TAIL(inf)
sentinel
null
1/9

Through the Example 1 portion, the queries return "hello", "hello", then "hello" and "leet", matching the expected output. No step touched more than two list nodes: every inc and dec works on the key's current bucket and one neighbor, and every query reads a single node next to a sentinel.

Code