432. 全 O(1) 的数据结构

考点

  • 哈希表与链表的综合
  • 掌握迭代器、pairunordered_setunordered_map的本质

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class AllOne {
private:
list <pair<int, unordered_set<string> > > l;
unordered_map <string, list <pair<int, unordered_set<string> > >::iterator> m;
public:
AllOne() {}

void inc(string key) {
auto it = m.find(key);
if (it == m.end()) {
//如果是新字符串
//链表为空,或头节点次数不为1;则需新建头节点
if (l.empty() || l.front().first > 1) {
l.emplace_front(1, unordered_set<string> {key});
} else {
//头节点次数为1,新字符串直接插入即可
l.front().second.emplace(key);
}
m[key] = l.begin();
} else {
auto node = it->second, nextNode = next(node);
if (nextNode == l.end() || nextNode->first > node->first + 1) {
//如果字符串所在的节点是链表末尾
//或者下一个节点的次数大于当前次数+1
//说明需要新建节点
m[key] = l.emplace(nextNode, node->first + 1, unordered_set<string> {key});
} else {
//下一个节点次数等于当前次数+1,直接插入即可
nextNode->second.emplace(key);
m[key] = nextNode;
}
//删除旧地方的字符串
node->second.erase(key);
if (node->second.empty()) l.erase(node);
}
}

void dec(string key) {
auto node = m[key], prevNode = prev(node);
if (node->first != 1) {
if (node == l.begin() || prevNode->first < node->first - 1) {
//前一个节点的次数小于当前次数-1
//或者前一个节点为空
//说明需要新建节点
m[key] = l.emplace(node, node->first - 1, unordered_set<string> {key});
} else {
//上一个节点次数等于当前次数-1,直接插入即可
prevNode->second.emplace(key);
m[key] = prevNode;
}
} else {
//如果节点次数为1,直接删除
m.erase(key);
}
//删除旧地方的字符串
node->second.erase(key);
if (node->second.empty()) l.erase(node);
}

string getMaxKey() {
return l.empty() ? "" : *(l.back().second.begin());
}

string getMinKey() {
return l.empty() ? "" : *(l.front().second.begin());
}
};

思路

本题难度极高,对哈希表、链表、迭代器的熟练度都是极大的考验,是非常棒的综合题!

在完成本题前,请先完成460. LFU 缓存146. LRU 缓存,掌握基本的哈希表与链表的结合思路

题目希望给每个字符串计数,可增可减,所有操作为常数时间复杂度;并返回计数最大与最小的字符串,如果计数为0,则将其删除

  1. 可以参考LFU,只使用变量来记录极值吗?

    LFU是单向操作,只增无减,统计极值用变量即可胜任

    本题有增有减,只用变量很难记录空间的动态变化,必须依赖容器来完成这一操作

  2. 可以参考LFU的数据结构吗?

    若参考LFU,使用unordered_map,则键为次数,给每个键单独新建链表作为值

    那么只能用变量来记录键的极值,因为unordered_map是无序的,方才提过用变量是死路

    若使用map,键虽有序但达不到常数时间复杂度

    故而LFU的数据结构无法照搬

联想LRU用到的有序双向链表技巧,表头是最晚访问元素,表尾是最早访问元素,极值就在两侧

可以设计数据结构如下:

  • 新建一个双向链表,链表节点为键值对,键为次数,值为unordered_set以存储字符串

    次数增加,则在当前节点之后新增节点/修改下一个节点;次数减少,则在当前节点之前新增节点/修改前一个节点

    从而保证了链表表头为次数最小值,链表表尾为次数最大值

  • 新建一个哈希表,键为字符串,值为对应次数的链表节点地址

  1. 为什么设计“左小右大”?而不是像LRU一样”左大右小“?

    若是左大右小,每次新增的节点都将在链表表尾,返回表尾元素的迭代器则是rbegin方法,且这个迭代器是reverse_iterator类型

    reverse_iterator类型是对iterator类型的封装,可参见

    双向链表list删除节点时,使用的是erase方法,它只支持iterator类型参数

    返回表头元素迭代器的方法begin,满足这一条件。故而选择左小右大

  2. 为什么双向链表的节点不选择unordered_map,而选择pair?

    因为unordered_mapmap都没有取出键这一个操作,哈希表的意义就是通过键访问值

    实际上,我们需要的是一个类似结构体的容器,第一个元素存储次数,第二个元素存储unordered_set

    这样一来前后节点比较次数时,直接取节点容器的第一个元素对比即可

    pair恰好满足,当然你乐意的话可以自写结构体

  3. 为什么存储字符串选择unordered_set,而不是vector

    因为字符串在不同节点之间转移时,所经历的步骤就是

    1. 从原节点存储删除
    2. 插入新节点存储

    知道一个值,怎么在常数时间复杂度从存储中删除呢?只有哈希表了!

    哈希表当中键与值相等的是什么STL?当然是unordered_setset