文章目录
题目描述
哈希表+手写双向链表
首先,确定好合适的数据结构:双向链表和哈希表(和LeetCode 460. LFU缓存类似的思路,自定义节点并手写双向链表来实现)
-
链表中的每个节点存储一个字符串集合 keys \textit{keys} keys 和一个正整数 cnt \textit{cnt} cnt,表示 keys \textit{keys} keys 中的字符串均出现 cnt \textit{cnt} cnt 次。链表从头到尾的每个节点的 cnt \textit{cnt} cnt 值单调递增(但不一定连续)。此外,每个节点还需存储指向上一个节点的指针 prev \textit{prev} prev 和指向下一个节点的指针 next \textit{next} next。
-
用一个哈希表 nodes \textit{nodes} nodes 维护每个字符串当前所处的链表节点。以字符串为哈希表的键,字符串所在
Node
作为值
其次,定义好数据结构后,对于每一个函数操作分情况讨论即可:
- 对于
inc
函数:- 若
key
不在链表中:- 若链表为空或头节点的 c n t > 1 cnt>1 cnt>1,则先插入一个 cnt = 1 \textit{cnt}=1 cnt=1 的新节点至链表头部,然后将 key \textit{key} key 插入到新加的头节点的 keys \textit{keys} keys 中
- 否则,直接将
key
放入root.next.keys
中,并更新nodes
中key
所在的节点
- 若
key
在链表中:设key
所在节点为cur
,- 若
cur.next
为空或 cur . next . cnt > cur . cnt + 1 \textit{cur}.\textit{next}.\textit{cnt}>\textit{cur}.\textit{cnt}+1 cur.next.cnt>cur.cnt+1,则先插入一个 cnt = cur . cnt + 1 \textit{cnt}=\textit{cur}.\textit{cnt}+1 cnt=cur.cnt+1 的新节点至 cur \textit{cur} cur 之后,然后将 key \textit{key} key 插入到cur.next.keys
中 - 否则,直接将
key
放入cur.next.keys
中,并更新nodes
中key
所在的节点 - 最后,将
key
从cur.keys
中移除,若移除后cur.keys
为空,则将cur
从链表中移除。
- 若
- 更新
nodes
中key
所处的节点
- 若
- 对于
dec
函数,测试用例保证key
在链表中:- 若
key
仅出现一次:将其从nodes
中移除 - 若
key
出现不止一次:设key
所在节点为cur
,- 若
cur.prev
为空或 cur . prev . cnt < cur . cnt − 1 \textit{cur}.\textit{prev}.\textit{cnt}<\textit{cur}.\textit{cnt}-1 cur.prev.cnt<cur.cnt−1,则先插入一个 cnt = cur . cnt − 1 \textit{cnt}=\textit{cur}.\textit{cnt}-1 cnt=cur.cnt−1 的新节点至cur
之前,然后将key
插入到新加节点的keys
中 - 否则,直接将
key
放入cur.prev.keys
中,并更新nodes
中key
所在的节点
- 若
- 最后,将
key
从cur.keys
中移除,若移除后cur.keys
为空,则将cur
从链表中移除。
- 若
- 对于
getMaxKey
函数:- 在链表不为空时,返回链表尾节点的
keys
中的任一元素, - 否则返回空字符串
- 在链表不为空时,返回链表尾节点的
- 对于
getMinKey
函数:- 在链表不为空时,返回链表头节点的
keys
中的任一元素, - 否则返回空字符串
- 在链表不为空时,返回链表头节点的
整体的存储结构示意图:
class AllOne {
// 双向链表节点
class Node {
Node prev;
Node next;
Set<String> keys;
int cnt;
public Node() {
this("", 0);
}
public Node(String key, int cnt) {
this.cnt =cnt;
keys = new HashSet<>();
keys.add(key);
}
// 在 this 后插入节点 node 并返回节点 node
public Node insert(Node node) {
// 插入 node
node.prev = this;
node.next = this.next;
// 修改原节点指向
node.prev.next = node;
node.next.prev = node;
return node;
}
// 在链表中删除 this 节点
public void remove() {
this.prev.next = this.next;
this.next.prev = this.prev;
}
}
// 以字符串为哈希表的键,字符串所在 Node 作为值
Map<String, Node> nodes;
// 哨兵节点
Node root;
public AllOne() {
root = new Node();
// 初始化链表哨兵,下面判断节点的 next 若为 root,则表示 next 为空(prev 同理)
root.prev = root;
root.next = root;
nodes = new HashMap<>();
}
public void inc(String key) {
// 分情况讨论
// key 在链表中
if (nodes.containsKey(key)) {
Node cur = nodes.get(key);
Node nxt = cur.next;
if (nxt == root || nxt.cnt > cur.cnt + 1) {
// 在 cur 节点后插入新节点
nodes.put(key, cur.insert(new Node(key, cur.cnt + 1)));
} else {
// 将 key 放入 cur.next 节点
nxt.keys.add(key);
// 更新 map 中 key 所在的节点
nodes.put(key, nxt);
}
// 移除 cur 节点中的 key
cur.keys.remove(key);
if (cur.keys.isEmpty()) {
// 移除链表中的 cur 节点
cur.remove();
}
} else { // key 不在链表中 头一次插入
if (root.next == root || root.next.cnt > 1) {
// 链表从头到尾的每个节点的 cnt 值单调递增(但不一定连续)
// 在 root 节点后插入新节点
nodes.put(key, root.insert(new Node(key, 1)));
} else {
// 将 key 放入 root.next 节点
root.next.keys.add(key);
// 更新 map 中 key 所在的节点
nodes.put(key, root.next);
}
}
}
public void dec(String key) {
Node cur = nodes.get(key);
// 分情况讨论
if (cur.cnt == 1) {
// key 仅出现一次,将其移出 nodes
nodes.remove(key);
} else {
Node pre = cur.prev;
if (pre == root || pre.cnt < cur.cnt - 1) {
// 在 cur.prev 节点后插入新节点
nodes.put(key, cur.prev.insert(new Node(key, cur.cnt - 1)));
} else {
// 将 key 放入 cur.prev 节点
pre.keys.add(key);
// 更新 map 中 key 所在的节点
nodes.put(key, pre);
}
}
// 移除 cur 节点中的 key
cur.keys.remove(key);
if (cur.keys.isEmpty()) {
// 移除链表中的 cur 节点
cur.remove();
}
}
public String getMaxKey() {
Node node = root.prev;
for (String str : node.keys) {
// 若链表不为空,返回 keys 中任一元素
return str;
}
return "";
}
public String getMinKey() {
Node node = root.next;
for (String str : node.keys) {
return str;
}
return "";
}
}
/**
* Your AllOne object will be instantiated and called as such:
* AllOne obj = new AllOne();
* obj.inc(key);
* obj.dec(key);
* String param_3 = obj.getMaxKey();
* String param_4 = obj.getMinKey();
*/
-
时间复杂度:所有操作均为 O ( 1 ) O(1) O(1),这里将字符串长度视作常数
-
空间复杂度: O ( I ) O(I) O(I),其中 I I I 是调用
inc
的次数。最坏情况下每次调用inc
传入的字符串均不相同,需要 O ( I ) O(I) O(I)大小的哈希表来存储所有字符串