1.LRU算法
核心思想:LRU算法(Least Recently Used)是一种常用的缓存淘汰策略,它的核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。LRU算法主要用于内存管理和缓存系统。当内存或缓存空间已满,需要腾出空间时,LRU算法会选择最近最久未使用的数据进行淘汰。
2.LRU算法的基本原理
LRU算法的基本原理是跟踪数据的访问时间,并在需要淘汰数据时选择最久未使用的数据。在实现上,最常见的方法是使用一个链表来保存缓存数据。新数据插入到链表头部,每当缓存命中(即缓存数据被访问),则将数据移到链表头部。当链表满的时候,将链表尾部的数据丢弃。
3.LeetCode中的实现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) 的平均时间复杂度运行。
1.首先我的想法是通过哈希表和双链表来实现,双链表负责节点的移动,哈希表负责快速定位节点。
struct LinkedNode{//自定义双链表
int key,value;
LinkedNode* prev;//前驱节点
LinkedNode* next;//后继结点
LinkedNode():key(0),value(0),prev(nullptr),next(nullptr){}
LinkedNode(int _key,int _value):key(_key),value(_value),prev(nullptr),next(nullptr){}
};
定义了一个双链表结构体,并定义了前驱节点和后继节点,双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的
在LRU的类中,我定义了虚拟的头尾节点,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在了。
unordered_map<int,LinkedNode*> cache;
LinkedNode* head;//首部
LinkedNode* tail;//尾部
int size;
int capacity;
之后,我定义了函数的有参构造,并让首尾指针互相指向。
LRUCache(int _capacity):capacity(_capacity),size(0) {//有参构造
//使用伪头部和伪尾部节点
head=new LinkedNode();
tail=new LinkedNode();
head->next=tail;
tail->prev=head;
}
之后就是get函数了,题目描述如:如果关键字 key
存在于缓存中,则返回关键字的值,否则返回 -1
;所以可以通过哈希表自带的函数count来检验key是否存在于哈希表中(时间复杂度0(1))
如果存在的话,因为LRU的特性(将最近访问的节点放到头部,最久不访问的放在尾部),就将node节点移动到头结点。
int get(int key) {
if(!cache.count(key)){//用于检查某一个值是否存在
return -1;
}
//如果key存在 先通过哈希表定位 再移动到头部
LinkedNode* node=cache[key];
moveToHead(node);//移动到头部**
return node->value;
}
put的题目描述如下:
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
向其中插入数据,如果不存在,就将其添加到哈希表<值,双链表指针>,并且放在头部,并判断此时的容量是否大于最大容量,若大于,淘汰尾部节点,并将节点对应的值从哈希表中移除。
如果存在,先根据key从哈希表中对应处节点,然后修改节点对应的值,最后将节点移动到头部。
void put(int key, int value) {
if(!cache.count(key)){//用于检查某一个值是否存在
//不存在
LinkedNode* node=new LinkedNode(key,value);
cache[key]=node;//添加到哈希表
addToHead(node); //添加到头部
++size;
if(size>capacity){
LinkedNode* removed=removeTail();
cache.erase(removed->key);
delete removed;
--size;
}
}else{
//如果key存在 先通过哈希表定位 在修改value,并且移动到头部
LinkedNode* node=cache[key];
node->value=value;
moveToHead(node);
}
}
最后就是定义移动节点的成员函数:
函数涉及到双指针的修改和插入操作,第一次我干看代码也没理解,知道画图出来才明白;
理解了如何添加节点之后,下面的也就难度不大了,要注意的一点是,在moveToHead函数中,不能改变俩个函数的位置,因为如果改变之后,就会报错内存错误--在释放堆内存之后继续使用。这是因为节点被错误地添加到头部后,又在未正确处理的情况下被删除。
void addToHead(LinkedNode* node){//添加到头结点
node->prev=head;
node->next=head->next;
head->next->prev=node;
head->next=node;
}
void removeNode(LinkedNode* node){//移除节点
node->prev->next=node->next;//双链表的删除方法
node->next->prev=node->prev;
}
void moveToHead(LinkedNode* node){//移动到头结点
removeNode(node);
addToHead(node);
}
LinkedNode* removeTail(){//移除尾节点
LinkedNode* node=tail->prev;
removeNode(node);
return node;
}