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)
的平均时间复杂度运行。
示例:
输入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4] 解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是 {1=1} lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4
思路详解:
我们使用双向链表和哈希表来联合解决这个问题,哈希表的键必须不是重复的,而且操作哈希表的时间复杂度为O(1),哈希表负责记录键和节点的映射关系,双向链表负责存储和修改数据,这里所有新加入的元素都会被重新置于双向链表的头,所有太久未被访问的元素都会置于链表尾部,通过查询并删除尾部节点就可以实现最近最少使用的办法。
代码详解:
struct Node{
int key,val;
Node*prev,*next;
Node():key(0),val(0),prev(nullptr),next(nullptr){}
Node(int keys,int vals):key(keys),val(vals),prev(nullptr),next(nullptr){}
};//构建一个双向链表的结构体,它包括链表的前驱,后继,构造函数,初始化键值的构造函数
class LRUCache {
public:
Node*head,*tail;
unordered_map<int,Node*> un_map;
int capacity,size;//定义链表的虚拟头尾节点,节点和key的哈希映射,当前容量和总容量
LRUCache(int _capacity):capacity(_capacity),size(0)
{
head=new Node();//初始化头尾节点,并让他们连接起来
tail=new Node();
head->next=tail;
tail->prev=head;
}
int get(int key) {
if(!un_map.count(key))//count函数用以查询哈希表中是否有对应的键,哈希表中的键值是不重复的
{
return -1;
}
Node*node=un_map[key];
removeNode(node);//查询缓存的时候将被查询的元素移除并重新插入回头节点
insertNode(node);//此方法被视为使用缓存中的数据,所以我们将它更新
return node->val;
}
void put(int key, int value) {
if(un_map.count(key))//如果存在修改值,并更新使用时间
{
Node*node=un_map[key];
node->val=value;
removeNode(node);
insertNode(node);
}
else
{
if(size>=capacity)//不存在的话先看使用内存是否已满,已满就移除最后一个元素
{
Node*remove=tail->prev;//获取要被移除的节点
un_map.erase(remove->key);//从哈希表中移除
removeNode(remove);//从链表中移除
size--;//已使用大小减一
}
Node*node=new Node(key,value);
insertNode(node);
un_map[key]=node;
size++;
}
}
void removeNode(Node*node)//方法,用于删除链表中节点
{
node->prev->next=node->next;//当前节点前驱的后继指向当前节点的后继
node->next->prev=node->prev;//当前节点后继的前驱指向当前节点的前驱
}
void insertNode(Node*node)//方法,用于插入元素至链表的头
{
node->prev=head;//当前节点的前驱是虚拟头节点
node->next=head->next;//当前元素的后继是虚拟头节点的后继
head->next->prev=node;//虚拟头节点的前驱是当前节点
head->next=node;//头节点的下一个节点是当前节点
}
};
面经:
1. 移动语义和拷贝语义有什么区别
- 拷贝语义会创建资源的副本。
- 移动语义会转移资源的所有权。
- 拷贝语义可能会引起较大的性能开销,尤其是涉及到大型数据结构时。
- 移动语义通常能提供更好的性能,因为它避免了不必要的数据复制。
- 在拷贝后,原对象保持不变。
- 在移动后,原对象可能不再可用或处于未定义状态。
2. 什么是智能指针,都有哪些智能指针
- C++11标准库引入了智能指针,它们是模板类,用于自动管理动态分配的内存,从而减少内存泄漏和其他资源管理错误的风险。智能指针确保资源在适当的时机被自动释放。
智能指针包括以下三种:
- unique_ptr
独占型智能指针,它保证同一时间只有一个指针指向所管理的对象。
当unique_ptr 被销毁时,它所管理的对象也会被自动销毁。
unique_ptr 不支持拷贝操作,但可以通过 move 将所有权转移给另一个unique_ptr。
- shared_ptr
共享型的智能指针,它允许多个指针共享同一个对象。
它使用引用计数机制来跟踪有多少个 shared_ptr 共享同一个对象。当最后一个shared_ptr 被销毁时,对象也会被自动销毁。shared_ptr 支持拷贝和赋值操作。
- weak_ptr:
弱引用智能指针,它用于解决 shared_ptr 可能造成的循环引用问题。
weak_ptr 必须配合 shared_ptr 使用,它不会增加引用计数。
当需要访问对象时,可以尝试通过weak_ptr 的 lock 方法来获取 shared_ptr,如果对象已经被销毁,则 lock 方法会返回一个空的shared_ptr。