题目地址:
https://leetcode.com/problems/lfu-cache/
实现一个Least Frequently Used (LFU) cache。它是一个类似HashMap的数据结构,存的是key-value pair,key唯一,并可以做三种操作:
1、初始化,它有一个最大容量;
2、int get(int key)
,得到key对应的value,如果key不存在则返回
−
1
-1
−1;
3、void put(int key, int value)
,如果key不存在,则添加该key-value pair,并且如果当前已经存了最大容量个元素了,就需要将cache里使用次数最少的元素删掉,如果有多个,则把最近使用时间最久的元素删掉,这里的“使用”的含义是这个元素被get过或put过,都算;如果key存在,则更新其value。
思路是HashMap + 双链表。开两个HashMap,一个存key和Node的对应关系,其中Node里存key对应的value以及该key被使用的次数,另一个存使用次数和该使用次数对应的所有Node所组成的链表,之所以要用链表来存,是因为要在使用次数相同的情况下删除最近使用时间最久的那个Node,可以借助链表来实现,链表头存最新的Node,链表尾存最旧的Node。并且,由于我们要找使用次数最小的那个链表,所以我们再维护一个变量minFreq专门存当前使用次数最少是多少次。
我们先考虑某个node被使用过一次之后,应该怎样处理它。我们开个函数叫update(Node node)
专门做这件事。其实现如下:
因为其freq会增加,所以我们先找到其freq对应的链表,然后将其在链表中删除,接着看一下minFreq是否要被更新,其被更新当且仅当当前使用次数为minFreq的节点都不存在了,即这个链表空了,并且它刚好就是对应的minFreq。如果是这样,就将minFreq增加
1
1
1(因为被update的节点的使用次数会等于minFreq加
1
1
1),再接着将node的使用次数加
1
1
1,再加入这个使用次数对应的链表的头部。当然如果该使用次数的链表不存在,则还需要将其建出来。
三个操作分别如下实现:
1、初始化两个HashMap,并存一下最大容量值。最好先在frequency map里先存一个使用次数
1
1
1对应的链表,因为添加新值的时候这个链表是一定会被用到的;
2、get的时候,先从key找到对应的node(找不到则直接返回
−
1
-1
−1),然后将其更新(执行update操作),最后返回该node里存的value值即可;
3、put的时候,如果最大容量是
0
0
0那就什么也不用做了,直接返回;接着先看一下key是否存在,如果存在,则将其对应的Node找出来,更新其value,并执行update;如果不存在,说明需要增加新Node了,此时要看一下是否达到了容量上限,如果达到了,则要执行删除Least Frequently Used元素的操作(该操作这样执行,先找到minFreq对应的链表,然后删掉其表尾node,并且将该node在key-Node的HashMap里也删掉。如果该链表空了,则说明minFreq的元素不存在了,但是这里事实上不需要更新minFreq,因为执行了删除Least Frequently Used元素的操作一定是因为有新元素进来,它的freq是
1
1
1,所以minFreq一定会变成
1
1
1,这可以在后面的逻辑里写上),删除完之后,将新节点new出来,加入key-Node的HashMap中,并将其加入使用次数为
1
1
1的链表表头,并且重置minFreq为
1
1
1。
代码如下:
class LFUCache {
private:
struct Node {
int key, val, freq;
Node *prev, *next;
Node(int key, int val)
: key(key), val(val), prev(nullptr), next(nullptr), freq(1) {}
};
struct LinkedList {
Node *head, *tail;
int sz;
LinkedList() : sz(0) {
head = new Node(0, 0);
tail = new Node(0, 0);
head->next = tail;
tail->prev = head;
}
void addFirst(Node *node) {
node->prev = head;
node->next = head->next;
head->next = node;
node->next->prev = node;
sz++;
}
void remove(Node *node) {
if (!sz) return;
node->prev->next = node->next;
node->next->prev = node->prev;
sz--;
}
Node *removeLast() {
if (!sz) return nullptr;
auto *res = tail->prev;
remove(res);
return res;
}
};
unordered_map<int, Node *> nodemp;
unordered_map<int, LinkedList *> freq;
int cap, minFreq;
void update(Node *node) {
auto *list = freq[node->freq];
list->remove(node);
// 如果该node是minFreq对应的唯一节点,则需要更新minFreq
if (node->freq == minFreq && !list->sz) minFreq++;
node->freq++;
if (!freq.count(node->freq)) freq[node->freq] = new LinkedList();
freq[node->freq]->addFirst(node);
}
// 这个是为了删除链表尾,即最近使用时间最久的元素
void removeOldest() {
auto *list = freq[minFreq];
auto *node = list->removeLast();
nodemp.erase(node->key);
}
public:
LFUCache(int capacity) {
// 使用次数为1的链表一开始就添进去,因为这个链表是一定会被用到的(除非cap = 0)
freq[1] = new LinkedList();
cap = capacity;
minFreq = 0;
}
int get(int key) {
if (!nodemp.count(key)) return -1;
auto *node = nodemp[key];
update(node);
return node->val;
}
void put(int key, int value) {
// 尤其要注意特判这个,否则nodeMap为空的时候会触发removeOldest操作,
// 会导致NullPointerException
if (!cap) return;
if (nodemp.count(key)) {
auto *node = nodemp[key];
node->val = value;
update(node);
return;
}
if (nodemp.size() == cap) removeOldest();
auto *node = new Node(key, value);
nodemp[key] = node;
freq[1]->addFirst(node);
minFreq = 1;
}
};
所有操作时间复杂度 O ( 1 ) O(1) O(1),空间 O ( n ) O(n) O(n), n n n是最大容量。