力扣146:LRU缓存
题目
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
- LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
- int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
- void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
思路
这是一个非常经典的面试题!!!
我们先看题目最后的要求,对get和put我们必须以O(1)的时间复杂度来完成,我们从题目中可以发现get和put我们都需要判断关键字是否存在所以对于查找这个过程我们就必须用O(1)来完成。很容易就能想到使用哈希表。那么关键是哈希表中的value我们使用什么类型呢?
我们再来看题目,get的操作很简单put前半段操作也很简单,主要是put的最后一句话:如果关键字数量超过capacity则应该逐出最久未使用的关键字。这个最久未使用的关键字要如何确认呢?什么又叫做最久未使用。这里我给出解释,我们使用get获得关键字也算是使用,我们使用put插入KV或者更改value这也叫做使用,所以这道题的关键是我们如何判断最久未使用的关键字是哪个。我们可以联想一下队列,队列是先进先出的,那么队列的队首元素不就一定程度上是最久未使用的元素吗?因为他第一个进入队列所以他第一个离开队列。那么我们是否也可以使用一个数据结构来演化出最新使用的在数据结构的这一端最久未使用的在数据结构的另一端。所以我们可以使用双向链表来完成这个功能,链表的节点中存储着kv。我们为链表增加一个哨兵头节点和哨兵尾节点用来方便后续的移动和删除。
在双向链表中链表头是最近使用的关键字节点链表尾是最久未使用的关键字节点,当我们使用get和put对一个关键字对应的value进行获得值,插入以及修改后我们将这个节点移动到链表头从而实时保证链表尾是最久未使用的关键字节点。
有了思路之后代码就好写了,我们需要先创造一个双向链表类,然后在LRU对象中增加哨兵头节点和哨兵尾节点以及容量和大小还有哈希表而哈希表的value就是链表的节点。
- 当我们使用get时先在哈希表中判断关键字是否存在,如果不存在我们就直接返回-1,存在我们就从哈希表中得到对应的节点之后将这个节点移到链表头最后返回节点的值。
- 当我们使用put时一样先在哈希表中判断关键字是否存在,如果不存在我们就使用kv直接创造一个新的节点之后判断链表的大小是否等于容量如果等于就要删除链表尾的节点,再将节点插入到链表头的位置以及将关键字添加到哈希表中,如果存在就修改对应节点中value的值再将节点移到链表头的位置上。
代码
cpp
// 双向链表
class LRUListNode {
public:
int _key, _value;
LRUListNode* _prev;
LRUListNode* _next;
LRUListNode() : _key(0), _value(0), _prev(NULL), _next(NULL) {};
LRUListNode(int key, int value)
: _key(key), _value(value), _prev(NULL), _next(NULL) {};
};
class LRUCache {
private:
LRUListNode* _head;
LRUListNode* _tail;
unordered_map<int, LRUListNode*> um;
int _capacity;
int _size;
public:
LRUCache(int capacity) : _capacity(capacity), _size(0) {
// 创造伪头部和伪尾部
_head = new LRUListNode();
_tail = new LRUListNode();
_head->_next = _tail;
_tail->_prev = _head;
}
int get(int key) {
// 如果哈希表中没有key对应的节点
if (!um.count(key)) {
// 直接返回-1
return -1;
}
// 如果有
else {
// 将该节点移到双向链表的头部
LRUListNode* node = um[key];
MovetoHead(node);
// 返回对应的value值
return node->_value;
}
}
void put(int key, int value) {
// 如果不存在对应节点
if (!um.count(key)) {
// 使用kv创造一个新节点
LRUListNode* newnode = new LRUListNode(key, value);
// 将新节点加入到哈希表中
um[key] = newnode;
// 如果双向链表的大小等于容量了
if (_size == _capacity) {
// 在双向链表中移除尾部节点
LRUListNode* node = RemoveTail();
// 在哈希表中移除尾部节点
um.erase(node->_key);
// 释放节点
delete node;
--_size;
}
// 将新节点插入到头部
AddtoHead(newnode);
_size++;
}
// 如果存在
else {
// 修改值
LRUListNode* node = um[key];
node->_value = value;
// 将节点移到头部
MovetoHead(node);
}
}
// 删除节点
void RemoveNode(LRUListNode* node) {
node->_prev->_next = node->_next;
node->_next->_prev = node->_prev;
}
// 删除尾部节点
LRUListNode* RemoveTail() {
LRUListNode* node = _tail->_prev;
RemoveNode(node);
return node;
}
// 在头部添加新节点
void AddtoHead(LRUListNode* node) {
node->_next = _head->_next;
node->_prev = _head;
_head->_next->_prev = node;
_head->_next = node;
}
// 将节点移到头部
void MovetoHead(LRUListNode* node) {
// 先移除再插入
RemoveNode(node);
AddtoHead(node);
}
};
/**
* 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);
*/