题目:
Design and implement a data structure for Least Frequently Used (LFU) cache. It should support the following operations: get
and put
.
get(key)
- Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value)
- Set or insert the value if the key is not already present. When the cache reaches its capacity, it should invalidate the least frequently used item before inserting a new item. For the purpose of this problem, when there is a tie (i.e., two or more keys that have the same frequency), the least recently used key would be evicted.
Follow up:
Could you do both operations in O(1) time complexity?
Example:
LFUCache cache = new LFUCache( 2 /* capacity */ ); cache.put(1, 1); cache.put(2, 2); cache.get(1); // returns 1 cache.put(3, 3); // evicts key 2 cache.get(2); // returns -1 (not found) cache.get(3); // returns 3. cache.put(4, 4); // evicts key 1. cache.get(1); // returns -1 (not found) cache.get(3); // returns 3 cache.get(4); // returns 4
思路:
记不清和前面哪道题目比较类似了,不过确实还是比较难的一道题目,O(1)的时间复杂度主要是通过哈希表、双向链表以及记录迭代器来实现的,下面我们分别详细介绍一下。
1)由于需要在O(1)的时间复杂度内通过key得到value值,所以需要建立key到value的哈希映射。但是由于每个key还对应着一个访问频率,所以为了简便起见,我们将值和访问频率组合成为一个pair {value, freq},定义一个哈希表m。
2)由于要在O(1)的时间内通过访问频率来定位需要删除的key值,所以我们定义一个从访问频率到key的哈希映射。但是由于同一个访问频率可能对应着多个key值,所以在该哈希表中,我们将value定义为一个双向列表。该哈希表名为fm。
3)由于我们需要在O(1)的时间内,对某一key值的访问频率进行修改,并且还不能破坏同一频率内的多个key值的有序性,所以我们还需要定义一个从key值到该key在访问频率哈希表中的迭代器,这样当有了key值之后,我们就可以直接定位到该值在频率哈希表中需要删除的迭代器,从而在O(1)的时间内将其删除。而由于双向列表的有序性,又可以保证在频率相同的情况下,删除掉最久没有用的那个key。
如果上面这三个关键的数据结构理解清楚了,那么这几个函数的实现就比较简单了:
1)get函数:调用一个get之后其访问频率要提高,所以我们首先修改fm,将该key从fm的原有访问频率对应的list中删除掉,并且加入到fm中新访问频率所对应的list中。
2)put函数:首先利用get函数试图访问该key值,如果该key值已经存在了,则直接修改value值即可(注意对访问频率的修改已经在get函数中完成了)。那么如果该key值还不存在呢?我们就首先检查是否超过容量了,如果是,则从哈希表m中删除掉最小频率所对应的双向列表的头结点对应的key值(头结点是同一访问频率中,最久没有被使用的),从fm中删除掉该key值,再从mIter中删除掉对应迭代器。最后就是添加新的item了。
我在下面的代码片段中给出了详细解释。有问题的读者欢迎在下面留言。
代码:
class LFUCache {
public:
LFUCache(int capacity) {
cap = capacity;
size = 0;
}
int get(int key) {
if(m.count(key) == 0) {
return -1;
}
fm[m[key].second].erase(mIter[key]); // frequency will increase, so erase it from fm
m[key].second++; // increase the frequency
fm[m[key].second].push_back(key); // add it to the new position in fm (at the end)
mIter[key] = --fm[m[key].second].end(); // the iterator should be the last in fm
if(fm[minFreq].size() == 0) {
++minFreq;
}
return m[key].first;
}
void put(int key, int value) {
if(cap <= 0) {
return;
}
int storedValue = get(key); // check whether this key exists
if(storedValue != -1) {
m[key].first = value; // modify the value (note the private data has been updated already)
return;
}
if(size >= cap) {
m.erase(fm[minFreq].front());
mIter.erase(fm[minFreq].front());
fm[minFreq].pop_front();
--size;
}
m[key] = {value, 1}; // add the new item
fm[1].push_back(key);
mIter[key] = --fm[1].end();
minFreq = 1;
size++;
}
private:
int cap;
int size;
int minFreq; // minFreq is the smallest frequency so far
unordered_map<int, pair<int, int>> m; // key to {value,freq};
unordered_map<int, list<int>::iterator> mIter; // key to list iterator, stores the key's position in the linked list;
unordered_map<int, list<int>> fm; // freq to key list;
};
/**
* Your LFUCache object will be instantiated and called as such:
* LFUCache obj = new LFUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/