实现 LRU 缓存算法
1 LRU 缓存介绍
LRU 算法全称是最近最少使用算法(Least Recently Use),是一种简单的缓存策略。顾名思义,LRU 算法会选出最近最少使用的数据进行淘汰。
那么什么是缓存呢?缓存专业点可以叫一种提高数据读取性能的技术,可以有效解决存储器性能和容量的矛盾,是一种空间换时间的设计思想,比如我们常见的内存是硬盘的缓存,Cache 是内存的缓存,浏览器本地存储是网络访问的缓存......
LRU 有许多应用场景,例如:
操作系统底层的内存管理。
缓存服务,例如 Redis,当数据满的时候就要淘汰掉长期不使用的 key,在 Redis 中用了一个类似的 LRU 算法,而不是严格的 LRU 算法。
MySQL 的 Buffer Pool,也就是缓冲池,它的目的是为了减少磁盘 IO。它是一块连续的内存,当 Buffer Pool 满的时候就要淘汰很久没有被访问过的页。
2 Leetcode 真题
146. LRU 缓存 [1],请你设计并实现一个满足 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) 的平均时间复杂度运行。
示例:
3 题目分析
1.首先,题目中提到函数 get 和 put 必须以 O(1) 的平均时间复杂度运行,很自然地我们可以想到应该使用哈希表。
2.其次,当访问数据结构中的某个 key 时,需要将这个 key 更新为最近使用;另外如果 capacity 已满,需要删除访问时间最早的那条数据。这要求数据是有序的,并且可以支持在任意位置快速插入和删除元素,链表可以满足这个要求。
3.结合 1,2 两点来看,我们可以采用哈希表 + 链表的结构实现 LRU 缓存。
如上图所示,就是哈希表 + 链表实现的 LRU 缓存数据结构,有以下几个问题解释一下:
1.为什么这里要使用双向链表,而不是单向链表?我们在找到了节点,需要删除节点的时候,如果使用单向链表的话,后驱节点的指针是直接能拿到的,但是这里要求时间复杂度是 O(1),要能够直接获取到前驱节点的指针,那么只能使用双向链表。
2.哈希表里面已经保存了 key ,那么链表中为什么还要存储 key 和 value 呢,只存入 value 不就行了?当我们删除节点的时候,除了需要删除链表中的节点,还需要删除哈希表中的节点。删除哈希表中的节点需要知道 key,所以在链表的节点中需要存储 key 和 value,当删除链表节点时拿到 key,再根据 key 到哈希表中删除节点。
3.虚拟头节点和虚拟尾节点有什么用?虚拟节点在链表中被广泛应用,又称为哨兵节点,通常不保存任何数据。使用虚拟节点我们可以统一处理链表中所有节点的插入删除操作,而不用考虑头尾两个节点的特殊情况。
4 代码实现
4.1 Golang
4.2 Java
4.3 运行结果
代码运行的返回结果如下,其中头尾两个 key=0, value=0 的节点是虚拟节点,请忽略。
5 测试案例示意图
第 1 步:初始化数据结构。
第 2 步:插入节点 key1。
第 3 步:插入节点 key2。 此时 key2 插入到链表头部。
第 4 步:插入节点 key3。 此时 key3 插入到链表头部。
第 5 步:插入节点 key4。当前 capacity 容量达到上限(3),分为 2 步:
使用 removeTail()
方法删除链表尾部的节点 key1,从 removeTail()
方法的返回值得到 node,再根据 node.key 得到 key1,然后去哈希表删除节点 key1。
然后插入节点 key4,此时 key4 在链表头部。
第 6 步:读取 key2 的值,将 key2 移动到链表头部。
6 参考资料
[1] 146. LRU 缓存: https://leetcode.cn/problems/lru-cache/
[2] 以 Leetcode 第 146 题为例学习 LRU 缓存算法: https://mp.weixin.qq.com/s/nI-rp3zTtei3TFIoBDcr5Q
[3] 从 leetcode 真题讲解手写 LRU 算法: http://www.xiaojieboshi.com/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E4%BB%8Eleetcode%E7%9C%9F%E9%A2%98%E8%AE%B2%E8%A7%A3%E6%89%8B%E5%86%99LRU%E7%AE%97%E6%B3%95.html#%E5%89%8D%E8%A8%80
[4] LRU 缓存机制: https://leetcode.cn/problems/lru-cache/solution/lruhuan-cun-ji-zhi-by-leetcode-solution/
[5] Java 集合系列之 LinkedHashMap: https://juejin.cn/post/6844903544152129550
[6] LinkedHashMap 基本原理和用法 &使用实现简单缓存: https://www.cnblogs.com/myseries/p/10774487.html
[7] LRU 算法及其优化策略——算法篇: https://juejin.cn/post/6844904049263771662#heading-3
7 欢迎关注
版权声明: 本文为 InfoQ 作者【Se7en】的原创文章。
原文链接:【http://xie.infoq.cn/article/a63b3bf64ca8de2b29d6e0042】。文章转载请联系作者。
评论