LRU算法介绍
LRU算法(Least Recently Used)算法实质上是一种缓存机制,常用于操作系统的缓存。计算机的缓存容量是有限的,如果缓存满了就要给新的内容腾位置。那淘汰哪些缓存,哪些继续以何种方式留在缓存中就是一个策略性的问题。LRU算法就是其中一种淘汰策略。其内核就是认为最近使用过的数据是最有用的,很久都没使用的数据是无用的,内存满了就应该优先淘汰很久没用过的数据。
举一个实际例子,安卓手机都可以把软件放到后台运行,比如我先后打开了「设置」「手机管家」「日历」,那么现在他们在后台排列的顺序是这样的:
但是这时候如果我访问了一下「设置」界面,那么「设置」就会被提前到第一个,变成这样:
假设我的手机只允许我同时开 3 个应用程序,现在已经满了。那么如果我新开了一个应用「时钟」,就必须关闭一个应用为「时钟」腾出一个位置,关那个呢?按照 LRU 的策略,就关最底下的「手机管家」,因为那是最久未使用的,然后把新开的应用放到最上面:
LRU算法的函数签名
函数签名可以参考146. LRU 缓存机制的题目要求:
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache
类:
LRUCache(int capacity)
以正整数作为容量 capacity 初始化 LRU 缓存int get(int key)
如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。void put(int key, int value)
如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:你是否可以在 O(1)
时间复杂度内完成这两种操作?
示例:
1 | 输入 |
提示:
- 1 <= capacity <= 3000
- 0 <= key <= 10000
- 0 <= value <= 105
- 最多调用 2 * 105 次 get 和 put
LRU算法设计
分析上面的操作过程,要让 put
和 get
方法的时间复杂度为 O(1),我们可以总结出 cache
这个数据结构必要的条件:
- 显然
cache
中的元素必须有顺序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。 - 我们要在
cache
中快速找某个key
是否已存在并得到对应的val
; - 每次访问
cache
中的某个key
,需要将这个元素变为最近使用的,也就是说cache
要支持在任意位置快速插入和删除元素。
那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap
。LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:
借助这个结构,我们来逐一分析上面的 3 个条件:
- 如果我们每次默认从链表尾部添加元素,那么显然越靠尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的。
- 对于某一个
key
,我们可以通过哈希表快速定位到链表中的节点,从而取得对应val
。 - 链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过
key
快速映射到任意一个链表节点,然后进行插入和删除。
这里先引出两个问题:为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 **key
**,为什么链表中还要存 key
和 val
呢,只存 val
不就行了?
代码实现
双链表的实现
首先实现连表的每个节点:
1 | type node struct { |
然后依靠着这些节点实现一个双链表:
1 | type DoubleList struct { |
双链表的几个API的实现
接下来我们实现操作双链表的几个API:
addLast(x *node)
:将最近使用的节点加入到双链表中(从尾部进入双链表)remove(x *node)
:删除双链表中的节点x(节点x一定存在的条件下)removeFirst()
:当容量满时,将最久未使用的节点(表首的节点)删除dlSize()
:返回双链表的节点数
addLast(x *node)
方法的实现细节
在双链表的尾部虚节点之前插入节点,需要注意插入过程中prev
和next
指针的插入顺序。
1 | func (dl *DoubleList) addLast(x *node) { // 在链表尾部添加节点 x,时间 O(1) |
remove(x *node)
方法的实现细节
删除连表中的某一节点,x存在时才能使用。
1 | func (dl *DoubleList) remove(x *node) { // 由于是双链表且给的是目标 Node 节点,时间 O(1) |
removeFirst()
方法的实现细节
缓存的空间占满,删除最久不使用节点(首部节点)。返回被删除的节点。
1 | func (dl *DoubleList) removeFirst() *node { // 删除链表中第一个节点,并返回该节点,时间 O(1) |
dlSize()
返回链表长度
1 | func (dl *DoubleList) dlSize() int { // 返回链表长度,时间 O(1) |
LRUCache类的实现
1 | type LRUCache struct { |
初始化:
1 | func Constructor(capacity) LRUCache { |
抽象后的中间层
为了避免直接对缓存进行操作时忘记同时对双链表和哈希表同时进行删除和插入操作,所以在具体的LRU的方法和数据结构之间添加一层作为抽象层(直接对数据结构进行操作)。
makeRecently(key int)
将某个节点提升为最近使用
当某个节点被使用后,变为最近使用节点,被放到链表尾部。
1 | func (this *LRUCache) makeRecently(key int) { |
addRecently(key int, val)
将某个新节点提升为最近使用
1 | func (this *LRUCache) addRecently(key int, val int) { |
deleteKey(key int)
删除某一个节点
1 | func (this *LRUCache) deleteKey(key int) { |
deleteLeastRecently()
删除最久未使用节点
1 | func (this *LRUCache) deleteLeastRecently() { |
LRUCache类的更新Get()和淘汰Put()方法
Get(key int)
方法:如果关键字 key
存在于缓存中,则返回关键字的值,否则返回 -1
。
1 | func (this *LRUCache) Get(key int) int { |
Put(int key, int value)
如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
1 | func (this *LRUCache) Put(key int, val int) { |
完整代码
1 | type node struct { |
- Post title:LRU算法
- Post author:洪笳淏
- Create time:2021-09-21 22:25:07
- Post link:https://jiahaohong1997.github.io/2021/09/21/LRU算法/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.