前言

LRU 是 Least Recently Used 的简写,字面意思则是最近最少使用

通常用于缓存的淘汰策略实现,由于缓存的内存非常宝贵,所以需要根据某种规则来剔除数据保证内存不被占满。

redis的数据淘汰策略中就包含LRU淘汰算法

如何实现一个完整的LRU缓存呢?这个缓存要满足:

  • 这个缓存要记录使用的顺序
  • 随着缓存的使用变化,要能更新缓存的顺序

基于这种特点,可以使用一个常用的数据结构:链表

  • 每次加入新的缓存时都添加到链表的头节点
  • 当缓存再次被使用时移动缓存到头节点
  • 当添加的缓存超过能够缓存的最大时,删除链表尾节点的元素

单链表和双向链表的选择

单链表的方向只有一个,节点中有一个next指针指向后继节点。而双链表有两个指针,可以支持两个方向,每个节点不止有一个next指针,还有一个prev指针指向前驱节点

双向链表需要额外的两个空间来存放前驱节点的指针prev和后继节点指针next,所以,存储相同大小的数据,双向链表需要更多的空间。虽然相比单向链表,双向链表的每个节点多个一个指针空间,但是这样的结构带来了更多的灵活性,在某些场景下非常适合使用这样的数据结构。删除和添加节点操作,双向链表的时间复杂度为O(1)

在单向链表中,删除和添加节点的时间复杂度已经是O(1)了,双向链表还能比单向链表更加高效吗?

先来看看删除操作

在删除操作中有两种情况:

  • 删除给定值的节点
  • 删除给定指针的节点

对于第一种情况,无论是删除给定值或者是给定的指针都需要从链表头开始依此遍历,直到找到所要删除的值

尽管删除这个操作的时间复杂度为O(1),但是删除的时间消耗主要是遍历节点,对应的时间复杂度为O(n),所以总的时间复杂度为O(n)。

对于第二种情况,已经给定了要删除的节点,如果使用单向链表,还得从链表头部开始遍历,直到找到待删除节点的前驱节点。但是对于双向链表来所,这就很有优势了,双向链表的待删除节点种包含了前驱节点,删除操作只需要O(1)的时间复杂度

同理对于添加操作:

我们如果想要在指定的节点前面或者后面插入一个元素,双向了链表就有很大的优势,他可以在O(1)的时间复杂度搞定,而单向链表还需要从头遍历。

所以,虽然双向链表比单向链表需要更多的存储空间,但是双向链表的应用更加广泛,JDK种LinkedHashMap这种数据结构就使用了双向链表

如何实现LRU缓存

单链表实现

下面我们基于单链表给出简单的代码实现:

  1. package com.ranger.lru;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. /**
  5. *
  6. * @author ranger
  7. * LRU缓存
  8. *
  9. */
  10. public class LRUMap<K,V> {
  11. /**
  12. * 定义链表节点
  13. * @author ranger
  14. *
  15. * @param <K>
  16. * @param <V>
  17. */
  18. private class Node<K, V> {
  19. private K key;
  20. private V value;
  21. Node<K, V> next;
  22. public Node(K key, V value) {
  23. this.key = key;
  24. this.value = value;
  25. }
  26. public Node() {
  27. }
  28. }
  29. /**
  30. * 缓存最大值
  31. */
  32. private int capacity;
  33. /**
  34. * 当前缓存数量
  35. */
  36. private int size;
  37. /**
  38. * 缓存链表头节点
  39. */
  40. private Node<K,V> head;
  41. /**
  42. * 缓存链表尾节点
  43. */
  44. private Node<K,V> tail;
  45. /**
  46. * 定义带参构造函数,构造一个为空的双向链表
  47. * @param capacity 缓存最大容量
  48. */
  49. public LRUMap(int capacity) {
  50. this.capacity = capacity;
  51. head = null;
  52. tail = null;
  53. size = 0;
  54. }
  55. /**
  56. * 无参构造函数,初始化容量为16
  57. */
  58. public LRUMap() {
  59. this(16);
  60. }
  61. /**
  62. * 向双向链表中添加节点
  63. * @param key
  64. * @param value
  65. */
  66. public void put(K key,V value) {
  67. addNode(key,value);
  68. }
  69. /**
  70. * 根据key获取缓存中的Value
  71. * @param key
  72. * @return
  73. */
  74. public V get(K key) {
  75. Node<K,V> retNode = getNode(key);
  76. if(retNode != null) {
  77. // 存在,插入头部
  78. moveToHead(retNode);
  79. return retNode.value;
  80. }
  81. // 不存在
  82. return null;
  83. }
  84. /**
  85. * 移动给定的节点到头节点
  86. * @param node
  87. */
  88. public void moveToHead(Node<K,V> node) {
  89. // 如果待移动节点是最后一个节点
  90. if(node == tail) {
  91. Node prev = head;
  92. while(prev.next != null && prev.next != node) {
  93. prev = prev.next;
  94. }
  95. tail = prev;
  96. node.next = head;
  97. head = node;
  98. prev.next = null;
  99. }else if(node == head){ // 如果是头节点
  100. return;
  101. }else {
  102. Node prev = head;
  103. while(prev.next != null && prev.next != node) {
  104. prev = prev.next;
  105. }
  106. prev.next = node.next;
  107. node.next = head;
  108. head = node;
  109. }
  110. }
  111. /**
  112. * 获取给定key的节点
  113. * @param key
  114. * @return
  115. */
  116. private Node<K,V> getNode(K key){
  117. if(isEmpty()) {
  118. throw new IllegalArgumentException("list is empty,cannot get node from it");
  119. }
  120. Node<K,V> cur = head;
  121. while(cur != null) {
  122. if(cur.key.equals(key)) {
  123. return cur;
  124. }
  125. cur = cur.next;
  126. }
  127. return null;
  128. }
  129. /**
  130. * 添加到头节点
  131. * @param key
  132. * @param value
  133. */
  134. private void addNode(K key,V value) {
  135. Node<K,V> node = new Node<>(key,value);
  136. // 如果容量满了,删除最后一个节点
  137. if(size == capacity) {
  138. delTail();
  139. }
  140. addHead(node);
  141. }
  142. /**
  143. * 删除最后一个节点
  144. */
  145. private void delTail() {
  146. if(isEmpty()) {
  147. throw new IllegalArgumentException("list is empty,cannot del from it");
  148. }
  149. // 只有一个元素
  150. if(tail == head) {
  151. tail = null;
  152. head = tail;
  153. }else {
  154. Node<K,V> prev = head;
  155. while(prev.next != null && prev.next != tail) {
  156. prev = prev.next;
  157. }
  158. prev.next = null;
  159. tail = prev;
  160. }
  161. size--;
  162. }
  163. /**
  164. * 链表是否为空
  165. * @return
  166. */
  167. private boolean isEmpty() {
  168. return size == 0;
  169. }
  170. /**
  171. * 添加节点到头头部
  172. * @param node
  173. */
  174. private void addHead(Node node) {
  175. // 如果链表为空
  176. if(head == null) {
  177. head = node;
  178. tail = head;
  179. }else {
  180. node.next = head;
  181. head = node;
  182. }
  183. size ++;
  184. }
  185. @Override
  186. public String toString() {
  187. StringBuilder sb = new StringBuilder();
  188. Node<K,V> cur = head;
  189. while(cur != null) {
  190. sb.append(cur.key)
  191. .append(":")
  192. .append(cur.value);
  193. if(cur.next != null) {
  194. sb.append("->");
  195. }
  196. cur = cur.next;
  197. }
  198. return sb.toString();
  199. }
  200. /**
  201. * 测试
  202. * @param args
  203. */
  204. public static void main(String[] args) {
  205. LRUMap<String,String> lruMap = new LRUMap(3) ;
  206. lruMap.put("1","tom") ;
  207. lruMap.put("2","lisa") ;
  208. lruMap.put("3","john") ;
  209. System.out.println(lruMap.toString());
  210. lruMap.put("4","july") ;
  211. System.out.println(lruMap.toString());
  212. lruMap.put("5","jack") ;
  213. System.out.println(lruMap.toString());
  214. String value = lruMap.get("3");
  215. System.out.println(lruMap.toString());
  216. System.out.println("the value is: "+value);
  217. String value1 = lruMap.get("1");
  218. System.out.println(value1);
  219. System.out.println(lruMap.toString());
  220. }
  221. }
  222. 输出结果:
  223. 3:john->2:lisa->1:tom
  224. 4:july->3:john->2:lisa
  225. 5:jack->4:july->3:john
  226. 3:john->5:jack->4:july
  227. the value is: john
  228. null
  229. 3:john->5:jack->4:july
LinkedHashMap实现

了解LinkedHashMap的都知道,它是基于链表实现,其中还有一个 accessOrder 成员变量,默认是 false,默认按照插入顺序排序,为 true 时按照访问顺序排序,也可以调用 构造函数传入accessOrder

LinkedHashMap 的排序方式有两种:

  • 根据写入顺序排序。
  • 根据访问顺序排序。

其中根据访问顺序排序时,每次 get 都会将访问的值移动到链表末尾,这样重复操作就能的到一个按照访问顺序排序的链表

我们可以重写LinkedHashMap中的removeEldestEntry方法来决定在添加节点的时候是否需要删除最久未使用的节点

代码实现如下:

  1. public class LRULinkedHashMap<K,V> {
  2. /**
  3. * 缓存map
  4. */
  5. private LinkedHashMap<K,V> cacheMap;
  6. /**
  7. * 当前缓存数量
  8. */
  9. private int size;
  10. /**
  11. * 构造一个cacheMap,并设置可以缓存的数量
  12. * @param size
  13. */
  14. public LRULinkedHashMap(int size) {
  15. this.size = size;
  16. cacheMap = new LinkedHashMap<K,V>(16,0.75F,true) {
  17. @Override
  18. // 重写方法,判断是否删除最久没使用的节点
  19. protected boolean removeEldestEntry(Map.Entry eldest) {
  20. if (size + 1 == cacheMap.size()){
  21. return true ;
  22. }else {
  23. return false ;
  24. }
  25. }
  26. };
  27. }
  28. /**
  29. * 添加缓存
  30. * @param key
  31. * @param value
  32. */
  33. public void put(K key,V value){
  34. cacheMap.put(key,value) ;
  35. }
  36. /**
  37. * 获取缓存
  38. * @param key
  39. * @return
  40. */
  41. public V get(K key){
  42. return cacheMap.get(key) ;
  43. }
  44. public String toString() {
  45. StringBuilder sb = new StringBuilder();
  46. Set<Entry<K, V>> entrySet = cacheMap.entrySet();
  47. for (Entry<K,V> entry : entrySet) {
  48. sb.append(entry.getKey())
  49. .append(":")
  50. .append(entry.getValue())
  51. .append("<-");
  52. }
  53. return sb.toString();
  54. }
  55. public static void main(String[] args) {
  56. LRULinkedHashMap<String,Integer> map = new LRULinkedHashMap(3) ;
  57. map.put("1",1);
  58. map.put("2",2);
  59. map.put("3",3);
  60. System.out.println(map);
  61. map.put("4", 4);
  62. System.out.println(map);
  63. }
  64. }

动手写一个LRU缓存的更多相关文章

  1. 动手实现一个 LRU cache

    前言 LRU 是 Least Recently Used 的简写,字面意思则是最近最少使用. 通常用于缓存的淘汰策略实现,由于缓存的内存非常宝贵,所以需要根据某种规则来剔除数据保证内存不被撑满. 如常 ...

  2. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  3. 动手写一个简单版的谷歌TPU-指令集

    系列目录 谷歌TPU概述和简化 基本单元-矩阵乘法阵列 基本单元-归一化和池化(待发布) TPU中的指令集 SimpleTPU实例: (计划中) 拓展 TPU的边界(规划中) 重新审视深度神经网络中的 ...

  4. 【redis前传】自己手写一个LRU策略 | redis淘汰策略

    title: 自己手写一个LRU策略 date: 2021-06-18 12:00:30 tags: - [redis] - [lru] categories: - [redis] permalink ...

  5. 动手写一个简单版的谷歌TPU-矩阵乘法和卷积

    谷歌TPU是一个设计良好的矩阵计算加速单元,可以很好的加速神经网络的计算.本系列文章将利用公开的TPU V1相关资料,对其进行一定的简化.推测和修改,来实际编写一个简单版本的谷歌TPU.计划实现到行为 ...

  6. 死磕 java同步系列之自己动手写一个锁Lock

    问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...

  7. 死磕 java线程系列之自己动手写一个线程池

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...

  8. 自己动手写一个服务网关-java

    自己动手写一个服务网关 原文链接:https://www.cnblogs.com/bigben0123/p/9252444.html 引言 什么是网关?为什么需要使用网关? 如图所示,在不使用网关的情 ...

  9. 动手写一个简单的Web框架(模板渲染)

    动手写一个简单的Web框架(模板渲染) 在百度上搜索jinja2,显示的大部分内容都是jinja2的渲染语法,这个不是Web框架需要做的事,最终,居然在Werkzeug的官方文档里找到模板渲染的代码. ...

随机推荐

  1. python call函数

    call()函数本质上是将一个类的实例转换成一个函数,例如下列示例: class Sample: def __init__(self, x, y): self.x = x self.y = y def ...

  2. Windows alias 给cmd命令起别名

    场景: Linux的alias命令是个非常实用的工具,任何命令通过alias可以精简到很短,比如:alias l='ls -l' Windows也有alias类似的命令,就是:doskey,开启方法也 ...

  3. JDBC简单查询数据库

    注:图片如果损坏,点击文章链接:https://www.toutiao.com/i6543890367761089031/ 1.我们先新建一个数据库作为测试库 数据库名称为test.测试表为perso ...

  4. MacBookPro2021 M1-MAX电脑问题锦集

    MacBook2021 M1-MAXPro电脑问题锦集 问题1: 开启硬盘加密,开机闪屏 问题详述: 在系统偏好设置中,打开安全与隐私,在弹出窗口中切换到第二个页签(文件保险箱),启用文件保险箱功能, ...

  5. unity3d百度语音+图灵机器人

    using NAudio.Wave; using System; using System.Collections; using System.Collections.Generic; using S ...

  6. Cesium入门10 - 3D Tiles

    Cesium入门10 - 3D Tiles Cesium中文网:http://cesiumcn.org/ | 国内快速访问:http://cesium.coinidea.com/ 我们团队有时把Ces ...

  7. C# Reflection反射机制

    一.反射 什么是反射 .Net的应用程序由几个部分:'程序集(Assembly)'.'模块(Module)'.'类型(class)'组成: 反射提供一种编程的方式,让程序员可以在程序运行期获得这几个组 ...

  8. 谷歌浏览器和火狐浏览器如何查看HTTP协议

    谷歌浏览器和火狐浏览器如何查看HTTP协议 谷歌浏览器查看HTTP协议 火狐浏览器查看HTTP协议

  9. python 小兵之小技巧

    用for循环打印数字从1开始 for a in range(1,num+1): 用split切割字符串可以用索引选择部分 int(el.split("_")[1]) range 第 ...

  10. python 小兵(2)

    while 条件: 结构体 if=条件: 等于 while 条件: 结构体 else: print(int(Ture))    1 print(int(False))   0 切片顾头不顾尾 prin ...