力扣-146-LRU缓存
LRU(Least Recently Used)最近最少使用,缓存这个是在《操作系统》课程上学习过的概念,会有面试要求实现也有所耳闻
需要实现的方法有3个
-
初始化方法,以指定的正整数作为LRU缓存结构的初始化容量
-
get方法,如果键在缓存中,就返回键值;否则返回-1
-
put方法,key不存在直接插入键值;存在覆盖原值
如果插入导致超过容量,则逐出最久未使用的关键字
很明显前两个是比较容易实现的,关键就是如何记录最久未使用,以及用于实现的底层数据结构的选型
题解思路
数据结构选型
这里使用hashmap来优化双向链表查询效率低下的问题
为什么选择双向链表?因为这里涉及了很多头(插入)尾(删除)操作,如果只是单向链表的话,删除尾节点会非常麻烦,需要遍历一遍链表
为什么不用数组?数组访问快,但是插入删除就需要大量地移动元素操作,恰恰这里又有大量的插入删除操作
为什么不用栈和队列?因为栈和队列只能单位置操作(栈顶和队尾(不过说回来对头插入其实是可以的吧))
代码逻辑
-
get:如果key存在,则首先查找到这个节点(通过hash映射定位到链表位置),(维护最近使用记录)将节点移动到链表的头部,最后返回节点值
-
put:如果key不存在,则在链表头插入节点,插入完成后检查节点数量是否超过最大容量,超过则删除链表和hashmap中的对应记录
如果key存在,则找到后覆盖key值,同时将节点移动到链表头部
实现
这里有链表的技巧是:使用额外的“伪头部”和“伪尾部”标记界限,这样就不需要在插入和删除节点时检查相邻节点是否存在
第一遍,跟着敲写注释理解
// 自己实现一个双向链表 struct DLinkedNode { int key, value; DLinkedNode* prev; DLinkedNode* next; // 这是构造函数 // 我第一次见这种赋初值的语法 DLinkedNode() :key(0), value(0), prev(nullptr), next(nullptr) {} DLinkedNode(int _key, int _value) :key(_key), value(_value), prev(nullptr), next(nullptr) {} }; class LRUCache { private: // 用map二次封装了双向链表 // 这里的key值int是什么? unordered_map<int, DLinkedNode*> cache; DLinkedNode* head; DLinkedNode* tail; int size; // 为什么这里要有一个size变量?直接访问容器大小属性不行吗 int capacity; public: LRUCache(int _capacity) :capacity(_capacity), size(0) { // 使用头尾伪节点 head = new DLinkedNode(); tail = new DLinkedNode(); head->next = tail; tail->prev = head; } // 辅助方法,前两个一增一删又是为第三个移动到头部服务 void addToHead(DLinkedNode* node) { // 先插入node再改head node->prev = head; node->next = head->next; head->next->prev = node; head->next = node; } void removeNode(DLinkedNode* node) { node->prev->next = node->next; node->next->prev = node->prev; } void moveToHead(DLinkedNode* node) { removeNode(node); } DLinkedNode* removeTail() { // 将伪伪节点的前一个真伪节点删除,并返回被删除掉的元素 DLinkedNode* node = tail->prev; removeNode(node); return node; } int get(int key) { if (!cache.count(key)) { // 如果在map中对key技术不为0 // 说明key存在,返回-1 return -1; } // 如果存在,则先将节点移动到头部,再返回键值 // 将找到的节点复制一份,浅拷贝仅复制了引用 // 这里为什么要以这种方式创建一个节点?其实不用这个局部变量也可以吧 // hashmap可以用key值作为下标访问吗? DLinkedNode* node = cache[key]; moveToHead(node); return node->value; } void put(int key, int value) { if (!cache.count(key)) { // 如果key不存在 DLinkedNode* node = new DLinkedNode(key, value); // 向hash表添加还能这么添加吗 cache[key] = node; ++size; if (size > capacity) { // 如果新增节点后大小大于容量,就把链表中的最后一个删除 DLinkedNode* removed = removeTail(); // 删除哈希表中的元素 cache.erase(removed->key);// 链表中其实没有真的删除,只是失去了链接关系,那么垃圾回收是怎么执行的 // new完delete,防止内存泄露 delete removed; --size; } } else { // 如果key存在 DLinkedNode* node = cache[key]; node->value = value; moveToHead(node); } } };
第二遍,边瞄边写
上面第一遍的代码有些问题,主要是漏了两句语句
以下是第二遍的代码
// 自行实现一个双向链表 struct DLinkedList{ int key; int value; DLinkedList* prev; DLinkedList* next; // 这里结构体居然有构造函数,真是跟class越来越像了 // 下面分别是有参和无参的构造函数,而且是用的新语法 DLinkedList():key(0),value(0),prev(nullptr),next(nullptr){} DLinkedList(int _key,int _value):key(_key),value(_value),prev(nullptr),next(nullptr){} }; class LRUCache { // 本题中使用hashmap封装双向链表实现底层数据结构 // 最近最少使用,链表中的头表示最近使用的节点数据,顺序表示使用时间的远近 // 而hashmap则是为了加快链表的检索速度 private: unordered_map<int,DLinkedList*> map; DLinkedList* head; DLinkedList* tail; int size; int capacity; public: LRUCache(int _capacity):size(0),capacity(_capacity) { // 这里要对两个节点初始化 head = new DLinkedList(); tail = new DLinkedList(); head->next=tail; tail->prev=head; } // 三个辅助方法 void addToHead(DLinkedList* node){ // size++要不早写在这里?不要 node->prev = head; node->next = head->next; head->next->prev=node; head->next=node; } void removeNode(DLinkedList* node){ // node->prev->next=node->next; node->next->prev=node->prev; } void moveToHead(DLinkedList* node){ // removeNode(node); addToHead(node); } DLinkedList* removeTail(){ // 这里使用一个局部变量主要是为了删除后返回,不然删了之后就没有指针指向,就找不到了 DLinkedList* node = tail->prev; removeNode(node); return node; } int get(int key) { // 如果key不存在,返回-1 if(!map.count(key)){ return -1; }else{ DLinkedList* node = map[key]; moveToHead(node); return node->value; } // 如果key存在,返回key值并将节点移动到链表头 } void put(int key, int value) { // 如果key不存在,则插入节点到链表头 if(!map.count(key)){ DLinkedList* node = new DLinkedList(key,value); // 这里先插入到了哈希表而不是链表,中,如果有原值则会被覆盖 map[key]=node; addToHead(node); ++size; if(size>capacity){ DLinkedList* removed = removeTail(); map.erase(removed->key); delete removed; --size; } }else{ // 如果key存在就覆盖value,并且把节点移动到链表头 DLinkedList* temp = map[key]; temp->value = value; moveToHead(temp); } } };
感觉这题本质上更像是“实现某种数据结构”的题型
本文作者:YaosGHC
本文链接:https://www.cnblogs.com/yaocy/p/16469430.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步