对于开发者而言,缓存的接触必不可少,无论是浏览器缓存(如果是chrome浏览器,可以通过chrome:://cache查看),还是服务端的缓存(通过memcached或者redis等内存数据库)。缓存不仅可以加速用户的访问,同时也可以降低服务器的负载和压力。那么,了解常见的缓存淘汰算法的策略和原理就显得特别重要。
常见的缓存算法:
LRU缓存:
关于LRU缓存的原理,很多资料已经介绍的非常全面了,这里不再赘述。如:
https://zhuanlan.zhihu.com/p/34133067--LRU原理和Redis实现——一个今日头条的面试题
https://www.cnblogs.com/cpselvis/p/6272096.html--常见缓存算法和LRU的C++实现
设计思路:
如上图所示:我们可以使用hash表来存储key值,这样可以实现get(key)和put(key)两种操作的时间复杂度都为O(1),而hash表中的value指向双向链表实现的 LRU 的 Node 节点。这里采用双向链表的原因是:如果采用普通的单链表,则删除节点的时候需要从表头开始遍历查找,效率为O(n),采用双向链表可以直接改变节点的前驱的指针指向进行删除达到O(1)的效率。
下面的图演示了它的原理。其中 head 代表双向链表的表头,tail 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。
所以这里用到的数据结构为:
unordered_map
通过ListNode来自己构造的双向链表
LRU算法的主要操作流程为:
下面看一下leetcode 146 LRU缓存机制的一道题目:
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get
和 写入数据 put
。
获取数据 get(key)
- 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value)
- 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
进阶:
你是否可以在 O(1) 时间复杂度内完成这两种操作?
示例:
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
代码如下:
class LRUCache {
public:
struct ListNode {//使用结构体建立双向链表,包含前驱、后继、key-value和构造函数
ListNode *pre;
ListNode *next;
int key;
int val;
ListNode(int _key, int _val) : pre(NULL), next(NULL), key(_key), val(_val) {};
};
LRUCache(int capacity) : max_cnt(capacity), cnt(0) {
head = new ListNode(-1, -1);
tail = new ListNode(-1, -1);
head->next = tail;//首尾相接
tail->pre = head;
}
void update(ListNode *p) {//更新链表
if (p->next == tail)
return;
//将p与前后连接断开
p->pre->next = p->next;
p->next->pre = p->pre;
//将p插入尾节点
p->pre = tail->pre;
p->next = tail;
tail->pre->next = p;
tail->pre = p;
}
int get(int key) {//获取值
unordered_map::iterator it = m.find(key);
if (it == m.end())
return -1;
ListNode *p = it->second;
//提取p的value后更新p
update(p);
return p->val;
}
void put(int key, int value) {
if (max_cnt <= 0)
return;
unordered_map::iterator it = m.find(key);//查找key值是否存在
//先延长链表再判断,如果超出,则删除节点
if (it == m.end()) {//如果不存在,则放在双向链表头部,即链表尾
ListNode *p = new ListNode(key, value);//初始化key和value
m[key] = p;//建立新的map
//在尾部插入新节点
p->pre = tail->pre;
tail->pre->next = p;
tail->pre = p;
p->next = tail;
cnt++;//计数+1
if (cnt > max_cnt) {//如果计数大于了缓存最大值
//删除头结点
ListNode *pDel = head->next;
head->next = pDel->next;
pDel->next->pre = head;
//在链表中删除后,需要在map中也删除掉
unordered_map::iterator itDel = m.find(pDel->key);
m.erase(itDel);
//delete pDel;
cnt--;
}
}
else {//如果存在
ListNode *p = it->second;//因为map的second存储的是key对应的链表地址,所以将其赋给p
p->val = value;//计算p内存块中的value值
update(p);//更新p
}
}
private:
int max_cnt;//最大缓存数量
int cnt;//缓存计数
unordered_map m;//记录数据和其链表地址
ListNode *head;//链表头
ListNode *tail;//链表尾
};