1 相关前置知识(OS)
- 为什么需要页面置换算法:当进程运行时,若其访问的页面不在内存需要将其调入,但内存已无空闲空间时,就需要从内存中调出一页,换出到外存,选择调出哪个页面的算法就称为页面置换算法。页面的换入,换出需要磁盘I/O开销较大,因此要设计一个好的页面置换算法追求更低的缺页率。
- 常见的页面置换算法:OPT(最佳置换算法) FIFO(先进先出置换算法) LRU(最近最久未使用置换算法) CLOCK(时钟置换算法)
- 什么是LRU算法:简单来讲就是将一些元素放在一个容量固定的容器中进行存取,由于容器的容量有限,该容器就要保证那些最近才被用到的元素始终在容器内,而将已经很久没有用的元素剔除,实现容器内元素的动态维护。这种算法是一种缓存维护策略,因为缓存空间有限,让缓存中存储的都是最近才被用到的元素可以实现系统缓存的高效运作。
2 面试题 16.25. LRU 缓存
2.1 题面
设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。
它应该支持以下操作:
- 获取数据
get
和 写入数据put
。 - 获取数据
get(key)
- 如果密钥(key)
存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。 - 写入数据
put(key, value)
- 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。 - 注意:要求
get
和put
的时间复杂度为O(1)
2.2 示例
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
2.3 解法1 (双端队列+哈希表)
思路
- 使用双端队列维护,队尾是最近使用的,队头是最久未使用的,哈希表则维护key-value,此外注意在get时若元素存在则要重新将该元素移动到队列尾部,put若元素已存在,则要更新value,并将元素调整到队尾。
class LRUCache {
private:
int capacity;
std::deque<int> dq;
std::unordered_map<int, int> mp;
public:
LRUCache(int capacity) : capacity(capacity) {}
int get(int key) { // 若元素存在则将该元素移到队尾
if (mp.find(key) != mp.end()) {
dq.erase(std::remove(dq.begin(), dq.end(), key), dq.end());
dq.push_back(key);
return mp[key];
}
return -1;
}
void put(int key, int value) {
if (mp.find(key) != mp.end()) {// 如果 key 已存在,更新 value,并调整到队尾
mp[key] = value;
dq.erase(std::remove(dq.begin(), dq.end(), key), dq.end());
dq.push_back(key);
} else {
if (dq.size() == capacity) {
int lru_key = dq.front();
mp.erase(lru_key);
dq.pop_front();
}
mp[key] = value;
dq.push_back(key);
}
}
};
2.4 解法2
解法1的效率十分低,而且没有满足时间复杂度O(1)的条件,效率只击败了5%,我们想想要如何实现O(1),什么数据结构支持O(1)维护两端元素呢,我们可以用双向链表来实现。
思路
使用双向链表+哈希表维护,表头维护最近使用的元素,表尾维护最久未使用的元素。这里我们要使用双向链表实现在表头添加一个元素、删除某个节点这两个操作。
对于
get
操作,首先判断key
是否存在:如果key
不存在,则返回 -1;如果key
存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。对于
put
操作,首先判断key
是否存在:如果key
不存在,使用key
和value
创建一个新的节点,在双向链表的头部添加该节点,并将key
和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;如果key
存在,则与get
操作类似,先通过哈希表定位,再将对应的节点的值更新为value
,并将该节点移到双向链表的头部。
class Node {
public:
int key, value;
Node *prev, *next;
// 构造函数
Node() : key(0), value(0), prev(nullptr), next(nullptr) {}
Node(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
std::unordered_map<int, Node*> cache;
Node* dummy; // 头节点 表头维护最近使用的 表尾维护最久未使用的
int capacity;
// 删除一个节点
void remove(Node *p) {
p->prev->next = p->next;
p->next->prev = p->prev;
}
// 表头添加一个节点
void push_front(Node *p) {
p->prev = dummy;
p->next = dummy->next;
p->prev->next = p;
p->next->prev = p;
}
Node* get_node(int key) {
auto it = cache.find(key);
if (it == cache.end()) return nullptr;
auto node = it->second;
remove(node); // 将元素移出
push_front(node); // 放在表头
return node;
}
public:
LRUCache(int capacity) {
this->capacity = capacity;
dummy = new Node();
dummy->next = dummy;
dummy->prev = dummy;
}
int get(int key) {
auto node = get_node(key);
return node ? node->value : -1;
}
void put(int key, int value) {
auto node = get_node(key);
if (node) { //若存在该元素
node->value = value; // 更新value
return ;
}
cache[key] = node = new Node(key, value);
push_front(node); // 放在表头
if (cache.size() > capacity) { // 超容量
auto back = dummy->prev;
cache.erase(back->key);
remove(back); //去掉尾部元素
delete back; // 释放内存
}
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/