问题:
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:
你是否可以在 O(1) 时间复杂度内完成这两种操作?
这个问题是leetcode的第146题,同时也是我2020.4.24号面试网易游戏的时候的一道面试题。
首先学习一下STL容器list的使用:
List封装了链表。常用操作有:
assign() 给list赋值
back() 返回最后一个元素
begin() 返回指向第一个元素的迭代器
clear() 删除所有元素
empty() 如果list是空的则返回true
end() 返回末尾的迭代器
erase() 删除一个元素
front() 返回第一个元素
get_allocator() 返回list的配置器
insert() 插入一个元素到list中
max_size() 返回list能容纳的最大元素数量
merge() 合并两个list
pop_back() 删除最后一个元素
pop_front() 删除第一个元素
push_back() 在list的末尾添加一个元素
push_front() 在list的头部添加一个元素
rbegin() 返回指向第一个元素的逆向迭代器
remove() 从list删除元素
remove_if() 按指定条件删除元素
rend() 指向list末尾的逆向迭代器
resize() 改变list的大小
reverse() 把list的元素倒转
size() 返回list中的元素个数
sort() 给list排序
splice() 合并两个list
swap() 交换两个list
unique() 删除list中重复的元素
下面看具体代码:
#include <iostream>
#include <list>
#include <numeric>
#include <algorithm>
using namespace std;
list<int> ListInt;
list<int> ListChar;
int main(int argc, char const *argv[])
{
ListInt.push_front(2);
ListInt.push_front(1);
ListInt.push_back(3);
ListInt.push_back(4);
cout << "listint.begin()----listint.end():" << endl;
for (list<int>::iterator iter = ListInt.begin();iter != ListInt.end();iter++) {
cout << *iter << " ";
}
cout << endl;
cout << "listint.rbegin()----listint.rend():" <<endl;
for (list<int>::reverse_iterator iter = ListInt.rbegin();iter != ListInt.rend();iter++) {
cout << *iter << " ";
}
cout << endl;
int result = accumulate(ListInt.begin(),ListInt.end(),0);
cout << "Sum=" << result << endl;
cout <<"------------------"<<endl;
ListChar.push_front('A');
ListChar.push_front('B');
ListChar.push_back('x');
ListChar.push_back('y');
cout << "listchar.begin()---listchar.end(): " << endl;
for (list<int>::iterator iter = ListChar.begin();iter != ListChar.end();iter++) {
cout << char(*iter) << " ";
}
cout << endl;
list<int>::iterator m = max_element(ListChar.begin(),ListChar.end());
cout << "The maximum element in listchar is : " << char(*m) << endl;
return 0;
}
上述代码的运行结果如下:
看下一段代码,关于list的更多的用法:
#include <iostream>
#include <list>
using namespace std;
void put_list(list<int> l,string name) {
list<int>::iterator plist;
cout << "The contents of " << name << ":";
for (plist = l.begin();plist != l.end();plist++) {
cout << *plist << " ";
}
cout << endl;
}
int main(int argc, char const *argv[])
{
list<int> l1;
list<int> l2(10,6); // 初始化为10个6
list<int> l3(l2.begin(),--l2.end()); // 初始化为9个6
list<int>::iterator iter;
put_list(l1,"list1");
put_list(l2,"list2");
put_list(l3,"list3");
l1.push_back(2);
l1.push_back(4);
cout << "l1.push_back(2) and l1.push_back(4): " <<endl;
put_list(l1,"list1");
l1.insert(++l1.begin(),3,9); // 在2和4之间插入3个9
cout << "l1.insert(++l1.begin(),3,9):" << endl;
put_list(l1,"list1");
cout << "l1.front() = " << l1.front() << endl;
cout << "l1.end() = " << l1.back() << endl;
l1.pop_front();
l1.pop_back();
cout<<"l1.pop_front() and l1.pop_back():"<<endl;
put_list(l1,"list1");
// 清除l1中的第二个元素
l1.erase(++l1.begin());
cout<<"list1.erase(++list1.begin()):"<<endl;
put_list(l1,"list1");
// 对l2进行赋值并显示
l2.assign(8,1); // 让l2变为8个1
cout << "l2.assign(8,1): " << endl;
put_list(l2,"list2");
cout << "l1.max_size(): " << l1.max_size() << endl;
cout << "l1.size(): " << l1.size() << endl;
cout << "l1.empty(): " << l1.empty() << endl;
put_list(l1,"list1");
put_list(l3,"list3");
cout << "list1 > list3: " << (l1 > l3) << endl;
cout << "list1 < list3: " << (l1 < l3) << endl;
l1.sort();
put_list(l1,"list1");
l1.splice(++l1.begin(),l3); // 合并。将l3放在了l1的第二个位置
put_list(l1,"list1");
put_list(l3,"list3");
return 0;
}
运行结果如下:
下面开始LRU算法的实现。
LRU的关键是最近最少使用,就是在缓冲区满之后,如何找到这个最近最少使用,然后将其替换掉。
方法为:维护一个队列,每次替换队头的,同时每使用一次,就将其对应的数据放到队尾。
所以,我最先想到了如下代码实现:
class LRUCache {
public:
int capacity;
unordered_map<int,int> data; // 储存键值对数据
list<int> que; // 用来寻找最近最少使用的数据的key
LRUCache(int capacity) {
this->capacity = capacity;
}
int get(int key) {
if (data.count(key)) { // 如果找得到
put(key,data[key]); // 这句话的目的是完成对que链表的更新,因为get代表使用。
return data[key]; // 返回对应的value值
}
return -1; // 否则返回-1
}
void put(int key, int value) {
if (data.count(key)) {
data[key] = value; // 如果找的到,对value值进行修改
move_to_back(key); // 因为刚刚使用,所以将对应key值放到que的队尾
}
else if (data.size() < capacity) {
data[key] = value; // 找不到且容量够,不需要替换,则将该数据加入map
que.push_back(key); // 同时将key放到队尾
}
else {
int dele = que.front(); // 因为容量已满,选择要删除的key,即队头
que.pop_front(); // 弹出
unordered_map<int,int>::iterator del = data.find(dele); // 找到要删除的key对应的迭代器
data.erase(del); // 在map删掉对应键值对节点
put(key,value); // 此时容量未满,调用自身,完成put
}
}
void move_to_back(int key) { // 通过遍历,将对应key先删除,再push_back到队列尾
for (list<int>::iterator iter = que.begin();iter != que.end();iter++) {
if (*iter == key) {
que.erase(iter);
que.push_back(key);
return;
}
}
}
void display() { // 方便调试
cout << "data: " << endl;
for (unordered_map<int,int>::iterator iter = data.begin();iter != data.end();iter++) {
cout << iter->first << " " << iter->second << endl;
}
cout << "list: " << endl;
for (list<int>::iterator iter = que.begin();iter != que.end();iter++) {
cout << *iter <<" " ;
}
cout << endl;
}
};
此代码时间复杂度较高,为O(n),做不到O(1)。
对其进行优化,提出一种哈希链表的数据结构,将哈希表的查找速度与链表的有序性结合起来。
定义如下:
unordered_map<int,list<pair<int,int> >::iterator > data;
list<pair<int,int> > que;
其哈希表的值为一个指向list的迭代器,然后list内部的数据类型保存了要查找的键值对。即哈希表通过key值找到一个指向list中一个个体的指针,然后通过这个指针可以找到对应的value。这样做的目的是即可以快速查找,也可以让数据按照我们定义的规则排序。
代码如下:
class LRUCache {
public:
int capacity;
unordered_map<int,list<pair<int,int> >::iterator > data;
list<pair<int,int> > que;
LRUCache(int capacity) {
this->capacity = capacity;
}
int get(int key) {
auto it = data.find(key); // 智能指针
if (it == data.end()) // 如果找不到,返回-1
return -1;
int val = it->second->second; // val要先通过key找到一个list<pair<int,int> >::iterator,然后这个迭代器会指向一个pair,这个pair的第二个元素为val。
que.erase(it->second); // 先在链表中删除
que.push_back(make_pair(key,val)); // 再将其放在链表的尾部
it->second = --que.end(); // 再次建立,哈希表与链表之间的关系。注意,que.end()并不指向最后一个元素,--que.end()才指向最后一个。
return val;
}
void put(int key, int value) {
auto it = data.find(key);
if (it != data.end()) {
que.erase(it->second); // 找得到,就先删掉
}
que.push_back(make_pair(key,value)); // 数据加到链表里
data[key] = --que.end(); // 为新数据建议哈希表与链表之间的关系
if (que.size() > capacity) { // 若容量满
int key = que.front().first; // 找到链表首部的key
data.erase(key); // 通过key值再hash表删除数据
que.pop_front(); // 将链表首部的数据弹出
}
}
};
这样,这个算法的复杂度达到了O(1).