146. LRU 缓存
class LRUCache {
using KeyType = int;
using ValueType = int;
using ListType = list<pair<KeyType, ValueType>>;
using MapType = unordered_map<KeyType, ListType::iterator>;
MapType Map_;
ListType List_;
int Size_;
int Capacity_;
public:
LRUCache(int capacity) : Map_(), List_(), Capacity_(capacity),
Size_(0) {}
int get(int key) {
auto I = Map_.find(key);
if (I == Map_.end())
return -1;
int Value = I->second->second;
List_.erase(I->second);
List_.push_front(make_pair(key, Value));
Map_[key] = List_.begin();
return Value;
}
void put(int key, int value) {
auto I = Map_.find(key);
if (I == Map_.end()) {
if (Size_ == Capacity_) {
int Key = List_.back().first;
List_.pop_back();
Map_.erase(Key);
} else {
++Size_;
}
} else {
List_.erase(I->second);
}
List_.push_front(make_pair(key, value));
Map_[key] = List_.begin();
}
};
这个没什么好说的,这是接近 boost::lru_cache 的标准实现,map 联系 key 和 list iterator 以及 value 构成的 pair,list 记录 key 用以实现 LRU 操作。
注意,我之前写了一个这样的版本:
class LRUCache {
using KeyType = int;
using ValueType = int;
using ListType = list<pair<KeyType, ValueType>>;
using MapType = unordered_map<KeyType, ListType::iterator>;
// ...
};
这种写法的问题在于,list 创建对象的开销是比较大的,而 value 可能比较复杂,所以应该尽可能地把 value 存在 map 里面。详细参考这个回复。
二刷
class LRUCache {
using KeyType = int;
using ValueType = int;
using ListType = std::list<KeyType>;
using MapType = std::unordered_map<KeyType, std::pair<ListType::iterator, ValueType>>;
int Size_{};
int Capacity_{};
ListType List_;
MapType Map_;
public:
LRUCache(int capacity) : Capacity_(capacity) {}
int get(int key) {
auto It = Map_.find(key);
if (It == Map_.end())
return -1;
List_.erase(It->second.first);
List_.push_front(key);
auto Renewed = List_.begin();
It->second.first = Renewed;
return It->second.second;
}
void put(int key, int value) {
auto It = Map_.find(key);
if (It == Map_.end()) {
if (Size_ == Capacity_) {
// XXX end is PAST the real end element!!!
// auto Victim = List_.end();
int Key = List_.back();
Map_.erase(Key);
List_.pop_back();
} else {
++Size_;
}
} else {
List_.erase(It->second.first);
}
List_.push_front(key);
Map_[key] = std::make_pair(List_.begin(), value);
}
};
二刷的时候竟然写错了!千万要注意 end() 方法给出的迭代器是在元素之后的!所以在抛弃最久未使用元素的时候,就算真的要用迭代器,也应该用 rbegin()。但 erase() 实际上是不接受 rbegin() 的,所以这里也必须用到 pop_back() 来实现移除尾元素。
std::list::splice() 还是得关注一下,这个东西其实挺好玩的,主要就在于它可以替代各种 erase() 啊,push_front() 啊这种东西。并且有一点要注意的是:对 List 节点进行的 splice() 操作不会使得 Iterator 失效!所以我们完全不用再修改 Map_[key] 强行补写一句。
int get(int key) {
auto It = Map_.find(key);
if (It == Map_.end())
return -1;
List_.splice(List_.begin(), List_, It->second.first);
// There's no need to renew the iterator. It won't change under splice().
// It->second.first = Renewed;
return It->second.second;
}
而 std::list::emplace() 呢,它的返回值就是一个迭代器,所以对 Map_ 进行更新的操作可以一行写完。
int put(int key, int value) {
...
// Update the map in one single line.
Map_[key] = make_pair(List_.emplace(List_.begin(), key), value);
}
三刷
class LRUCache {
using KeyType = int;
using ValueType = int;
using Iterator = std::list<KeyType>::iterator;
std::list<KeyType> History_;
std::unordered_map<KeyType, std::pair<ValueType, Iterator>> Dict_;
int Capacity_;
int Size_;
public:
LRUCache(int capacity) {
Capacity_ = capacity;
Size_ = 0;
}
int get(int key) {
if (auto I = Dict_.find(key); I != Dict_.end()) {
History_.splice(History_.begin(), History_, I->second.second);
return I->second.first;
} else {
return -1;
}
}
void put(int key, int value) {
if (auto I = Dict_.find(key); I != Dict_.end()) {
I->second.first = value;
History_.erase(I->second.second);
} else {
if (Capacity_ == Size_) {
int Eliminate = History_.back();
Dict_.erase(Eliminate);
History_.pop_back();
} else {
++Size_;
}
}
Dict_[key] = make_pair(value, History_.emplace(History_.begin(), key));
}
};
这个写法可能还是最符合直觉。
对于History_的操作,list::splice()可以用在get()操作里面,反正是一行能解决的事情;但是如果要用到put()里面去的话,对控制流的结构就有要求了,所以put()里面还是乖乖用最直接的erase()、pop_back()等等,只在最后向Dict_里插入东西的时候用一下list::emplace(),这样可以直接得到迭代器。
此外要注意,put()操作无论如何必定会改变Dict_,所以我们更新Dict_的操作要放在整个控制流的最外层。