缓存淘汰算法 LRU 和 LFU
LRU (Least Recently Used), 即最近最少使用用算法,是一种常见的 Cache 页面置换算法,有利于提高 Cache 命中率。
LRU 的算法思想:对于每个页面,记录该页面自上一次被访问以来所经历的时间 \(t\),当淘汰一个页面时,应选择所有页面中其 \(t\) 值最大的页面,即内存中最近一段时间内最长时间未被使用的页面予以淘汰。
其余常见的页面置换算法还有:
- OPT:理想化的置换算法,假设 OS 知道程序后续访问的所有页面的顺序,每次淘汰页面时,OPT 选择的页面将是以后永不使用的,或是在最长(未来)时间内不再被访问的页面。采用 OPT 通常可保证最低的缺页率(最高的 Cache 命中率)。
- FIFO:先进先出
- Clock 置换,又称最近未用算法 (NRU,Not Recently Used) 。
- 算法思想:为每个页面设置一个访问位,再将内存中的页面都通过链接指针链接成一个环形队列。当某个页被访问时,其访问位置 1。当需要淘汰一个页面时,只需检查页的访问位。如果是 0,就选择该页换出;如果是 1,暂不换出,将访问位改为 0,继续检查下一个页面。若第一轮扫描中所有的页面都是 1,则将这些页面的访问位一次置为 0 后,再进行第二轮扫描,第二轮扫描中一定会有访问位为 0 的页面。
- LFU (Least Frequently Used) :为每个页面配置一个计数器,一旦某页被访问,则将其计数器的值加1,在需要选择一页置换时,则将选择其计数器值最小的页面,即内存中访问次数最少的页面进行淘汰。
LRU
例子
给定一个程序的页面访问序列:7 0 1 2 0 3 0 4,假设实际 Cache 只有 3 个页面大小,根据 LRU,画出 Cache 中的页面变化过程。
使用一个栈(大小是 3),栈顶总是最新访问的页面。当栈满时,最新访问页面为 x:
x不在栈当中,去除栈底元素,把x置入栈顶。x在栈当中,把x移至栈顶,其他页面顺序不变。
如下图所示,图源自知乎。

如果简单使用一个数组来模拟上述过程,访问一次页面的平均时间复杂度为 \(O(n)\) 。
实现
在这里,LRU 存放的数据是一个键值对 (key, val) 。
Leetcode 题目:146. LRU 缓存机制。
要求 get 和 put 操作都在 \(O(1)\) 时间内完成。
方法:双向链表+哈希。双向链表头尾各自带有一个 dummy 节点(可以简化插入、删除操作的代码)。
哈希表与链表的关系如图所示(图来源自 Leetcode 讨论区)。

需要保证 get 方法在 \(O(1)\) 内完成,因此哈希表只能使用 unordered_map 而不是 map 。
struct Node
{
int key, value;
Node *next, *prev;
Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
class List
{
public:
Node *head, *tail;
int length;
List() : length(0)
{
head = new Node(-1, -1), tail = new Node(-1, -1);
head->next = tail, tail->prev = head;
}
void pushFront(Node *node)
{
auto next = head->next;
head->next = node, node->next = next;
next->prev = node, node->prev = head;
length++;
}
void pushBack(Node *node)
{
auto prev = tail->prev;
prev->next = node, node->next = tail;
tail->prev = node, node->prev = prev;
length++;
}
bool empty() { return length == 0 && head->next == tail && tail->prev == head; }
Node *popFront()
{
if (empty()) return nullptr;
auto p = head->next;
head->next = p->next, p->next->prev = head;
p->next = p->prev = nullptr;
length--;
return p;
}
Node *popBack()
{
if (empty()) return nullptr;
auto p = tail->prev;
tail->prev = p->prev, p->prev->next = tail;
p->prev = p->next = nullptr;
length--;
return p;
}
Node *remove(Node *node)
{
if (node == nullptr || empty()) return nullptr;
auto prev = node->prev, next = node->next;
prev->next = next, next->prev = prev;
node->next = node->prev = nullptr;
length--;
return node;
}
};
class LRUCache
{
public:
unordered_map<int, Node *> table;
List list;
int capacity;
LRUCache(int capacity) { this->capacity = capacity; }
int get(int key)
{
if (table.count(key) == 0) return -1;
else
{
auto node = list.remove(table[key]);
list.pushFront(node);
return node->value;
}
}
void put(int key, int value)
{
if (table.count(key) == 0)
{
auto node = new Node(key, value);
table[key] = node;
if (list.length == capacity)
{
auto p = list.popBack();
table.erase(p->key);
delete p;
}
list.pushFront(node);
}
else
{
table[key]->value = value;
list.pushFront(list.remove(table[key]));
}
}
};
下面尝试使用 STL 中的 list 完成。
TODO.
LFU
Leetcode 题目:460. LFU 缓存。
LFU (Least Frequently Used) 的主要思想是为每个缓存项一个计数器,一旦某个缓存项被访问,则将其计数器的值加 1,在需要淘汰缓存项时,则将选择其计数器值最小的,即内存中访问次数最少的缓存进行淘汰。
按照这一描述,首先想到的是可以通过哈希表 + 优先队列(小顶堆),堆中的数据以缓存项的计数器作为键值排序。
对于 get 方法而言,通过哈希表找到 key 对应元素的位置,计数器加 1,重新调整堆,时间复杂度为 \(O(\log{n})\) .
对于 put 方法而言,如果缓存中存在该元素,计数器加一,重新调整堆,时间复杂度为 \(O(\log{n})\) ;如果不存在该元素并且缓存已满,那么直接把堆顶元素替换为新的元素 <key,value>(因为新元素的计数器为 1 ,必然也是最小的),时间复杂度为 \(O(1)\)。因此,put 方法总的时间复杂度为 \(O(\log{n})\) .
这一方法基于堆实现,无法保证当 2 个元素的计数器相同时,被淘汰的是「较旧」的元素。
现考虑 get 和 put 均为 \(O(1)\) 的解法。
考虑基于「哈希+十字链表」实现,如下图所示(出处见水印)。
链表节点定义为:
struct Node
{
int key, value, counter;
Node *prev, *next, *another;
};
hashmap 用于记录 key -> Node* 的映射关系,辅助 get 方法在 \(O(1)\) 时间内完成。
对于访问次数相同的节点,用链表连接起来(上图的横向链表),链表的头部记录访问次数,随后连接缓存数据的节点,数据节点有一个额外的指针 another 指向第一个节点(即记录访问次数的节点),然后把所有链表的头部也连接起来(上图的纵向链表),形成十字链表。
对于 get 方法,通过 p = hashmap[key] 找到指向数据的指针,然后把 p 移动到 count+1 的链表尾部(如果链表 count+1 不存在则插入一个)。
考虑 put 方法的最坏情况,如果 <key, val> 不在缓存当中, 并且缓存已满,那么就从十字链表的第一个链表(count 值最小的链表)删除尾部节点,并在头部插入新节点(这么做是为了按照从新到旧存储每个数据,尾部就是「最旧」的节点,可以做到计数相同的情况下,淘汰旧元素)。
但这种「十字链表」结构实现起来过于复杂(代码肯定不简洁),所以我们把「十字链表」转换为一个哈希表 hash<int, List> ,如下图所示。

代码实现:
struct Node
{
int key, value, counter;
Node *next, *prev;
Node(int k, int v) : key(k), value(v), counter(1), prev(nullptr), next(nullptr) {}
};
class List
{
public:
Node *head, *tail;
int length;
List() : length(0)
{
head = new Node(-1, -1), tail = new Node(-1, -1);
head->next = tail, tail->prev = head;
}
void pushFront(Node *node)
{
auto next = head->next;
head->next = node, node->next = next;
next->prev = node, node->prev = head;
length++;
}
void pushBack(Node *node)
{
auto prev = tail->prev;
prev->next = node, node->next = tail;
tail->prev = node, node->prev = prev;
length++;
}
bool empty() { return length == 0 && head->next == tail && tail->prev == head; }
Node *popFront()
{
if (empty()) return nullptr;
auto p = head->next;
head->next = p->next, p->next->prev = head;
p->next = p->prev = nullptr;
length--;
return p;
}
Node *popBack()
{
if (empty()) return nullptr;
auto p = tail->prev;
tail->prev = p->prev, p->prev->next = tail;
p->prev = p->next = nullptr;
length--;
return p;
}
Node *remove(Node *node)
{
if (node == nullptr || empty()) return nullptr;
auto prev = node->prev, next = node->next;
prev->next = next, next->prev = prev;
node->next = node->prev = nullptr;
length--;
return node;
}
};
class LFUCache
{
public:
unordered_map<int, Node *> table;
unordered_map<int, List> data;
int capacity, total, minCounter;
LFUCache(int capacity)
{
this->capacity = capacity;
this->total = 0;
this->minCounter = 1;
}
int get(int key)
{
if (table.count(key) == 0) return -1;
else
{
auto node = table[key];
int counter = node->counter;
node->counter++;
data[counter].remove(node);
data[counter + 1].pushFront(node);
if (data[counter].length == 0 && counter == minCounter)
minCounter = counter + 1;
return node->value;
}
}
void put(int key, int value)
{
// 面向测试用例编程,这个 capacity = 0 属实不讲武德
if (capacity == 0) return;
if (table.count(key) == 0)
{
auto node = new Node(key, value);
if (total == capacity)
{
auto p = data[minCounter].popBack();
table.erase(p->key);
total--;
delete p;
}
minCounter = 1;
table[key] = node;
data[1].pushFront(node);
total++;
}
else
{
auto node = table[key];
int counter = node->counter;
node->value = value, node->counter++;
data[counter].remove(node);
data[counter + 1].pushFront(node);
if (data[counter].length == 0 && counter == minCounter)
minCounter = counter + 1;
}
}
};
缓存淘汰算法 LRU 和 LFU的更多相关文章
- 图解缓存淘汰算法二之LFU
1.概念分析 LFU(Least Frequently Used)即最近最不常用.从名字上来分析,这是一个基于访问频率的算法.与LRU不同,LRU是基于时间的,会将时间上最不常访问的数据淘汰;LFU为 ...
- 聊聊缓存淘汰算法-LRU 实现原理
前言 我们常用缓存提升数据查询速度,由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,这样新数据才可以添加进来.缓存数据不能随机删除,一般情况下我们需要根据某种算法删除缓存数据.常用淘 ...
- 淘汰算法 LRU、LFU和FIFO
含义: FIFO:First In First Out,先进先出LRU:Least Recently Used,最近最少使用 LFU:Least Frequently Used,最不经常使用 以上三者 ...
- 缓存淘汰算法--LRU算法
1. LRU1.1. 原理 LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是"如果数据最近被访问过,那么将来被访问的几率也 ...
- 缓存淘汰算法---LRU
1. LRU1.1. 原理 LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”. ...
- 缓存淘汰算法---LRU转
1. LRU1.1. 原理 LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”. ...
- 缓存淘汰算法--LRU算法(转)
(转自:http://flychao88.iteye.com/blog/1977653) 1. LRU1.1. 原理 LRU(Least recently used,最近最少使用)算法根据数据的历史访 ...
- 昨天面试被问到的 缓存淘汰算法FIFO、LRU、LFU及Java实现
缓存淘汰算法 在高并发.高性能的质量要求不断提高时,我们首先会想到的就是利用缓存予以应对. 第一次请求时把计算好的结果存放在缓存中,下次遇到同样的请求时,把之前保存在缓存中的数据直接拿来使用. 但是, ...
- 04 | 链表(上):如何实现LRU缓存淘汰算法?
今天我们来聊聊“链表(Linked list)”这个数据结构.学习链表有什么用呢?为了回答这个问题,我们先来讨论一个经典的链表应用场景,那就是+LRU+缓存淘汰算法. 缓存是一种提高数据读取性能的技术 ...
随机推荐
- .NET Core集成CorrelationId实现全链路日志输出
.NET Core集成CorrelationId实现全链路日志输出 一,链路追踪 随着微服务架构的流行,一次请求会涉及多个服务的调用,并且服务本身也可能会依赖其他服务,整个请求路径会构成一个调用链,当 ...
- pip install leveldb 编译错误解决
centos7,python3.3 # pip-python3 install leveldb 错误: /usr/include/python3.3m/dynamic_annotations.h:47 ...
- Spark性能调优篇六之调节数据本地化等待时长
数据本地化等待时长调节的优化 在项目该如何使用? 通过 spark.locality.wait 参数进行设置,默认为3s,6s,10s. 项目中代码展示: new SparkConf().set(&q ...
- ECharts的基本使用与方法
ECharts,一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器,底层依赖轻量级的矢量图形库 ZRender,提供直观,交互丰富,可高度 ...
- Numpy的学习2-基础运算1
import numpy as np a=np.array([10,20,30,40]) # array([10, 20, 30, 40]) b=np.arange(4) # array([0, 1, ...
- Linux系列之makefile的简单入门
什么是makefile呢? 一个工程中的源文件不计其数,其按类型.功能.模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译, ...
- 【代码周边】Idea设置类注解和方法注解(带图)
Idea版本: 类注解 打开setting→Editor→Code Style→File and Code Templates /** * Created with IntelliJ IDEA. * ...
- jQuery 库中的 $() 是什么?
概述: $() 函数是 jQuery() 函数的别称. $() 函数用于将任何对象包裹成 jQuery 对象,接着就被允许调用定义在 jQuery 对象上的多个不同方法. 可以将一个选择器字符串传入 ...
- 安装交叉编译工具arm-linux-gcc-4.3.2 并且修改环境变量
安装交叉编译工具arm-linux-gcc-4.3.2 2011-07-08 00:55:28| 分类: 嵌入式|举报|字号 订阅 环境:Fedora 9STEP 1: 下载arm-linu ...
- json 与 ajax
json类似与js中的对象,但是json中不能有方法,json相当于python中的字典,但是json中的键值如果是字符串的话,需要加上双引号:ajax是一个前后台配合的技术,它可以让js发送http ...