社区编辑申请
注册/登录
手写最简单的LRU算法
开发 前端 算法
LRU(Least recently used)最近最少使用,它的核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。因此 LRU 算法会根据数据的历史访问记录来进行排序,如果空间不足,就会淘汰掉最近最少使用的数据。

1 什么是LRU

LRU(Least recently used)最近最少使用,它的核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。因此 LRU 算法会根据数据的历史访问记录来进行排序,如果空间不足,就会淘汰掉最近最少使用的数据。

2 LRU 实现原理

由于 LRU 算法会将最近使用的数据优先级上升,因此需要数据结构支持排序,链表非常合适。

为什么不考虑数组呢?

由于 LRU 算法,一般都会应用在访问比较频繁的场景,因此,对数据的移动会频繁,而数组一旦移动,需要将移动到值的位置后面的所有数据的位置全部改变,效率比较低,不推荐使用。

3 双向链表之LinkedHashMap

前面我们分析到 LRU 的算法实现,可以使用链表实现,java 中 LinkedHashMap 就是一个双向链表。

LinkedHashMap是HashMap的子类,在HashMap数据结构的基础上,还维护着一个双向链表链接所有entry,这个链表定义了迭代顺序,通常是数据插入的顺序。

我们来看看LinkedHashMap的源码:

从源码中的定义可以看到,accessOrder 属性可以指定遍历 LinkedHashMap 的顺序,true 表示按照访问顺序,false 表示按照插入顺序,默认为 false。

由于LRU对访问顺序敏感,因此使用true来简单验证一下:

  1. public class LRUTest { 
  2.     public static void main(String[] args) { 
  3.         LinkedHashMap<String, Object> map = new LinkedHashMap<>(16, 0.75f, true); 
  4.         map.put("a", 1); 
  5.         map.put("b", 2); 
  6.         map.put("c", 3); 
  7.         System.out.println("before get " + map); 
  8.         map.get("a"); 
  9.         System.out.println("after get" + map); 
  10.     }} 

运行结果如下:

  1. before get {a=1, b=2, c=3} 
  2. after get{b=2, c=3, a=1} 

可以看到通过 accessOrder = true,可以让 LinkedHashMap 按照访问顺序进行排序。

那么 LinkedHashMap 是怎么做的呢?

我们看下get方法

  1. public V get(Object key) { 
  2.     Node<K,V> e; 
  3.     // 获取node 
  4.     if ((e = getNode(hash(key), key)) == null
  5.         return null
  6.     // 如果 accessOrder = true,则执行afterNodeAccess方法 
  7.     if (accessOrder) 
  8.         afterNodeAccess(e); 
  9.     return e.value; 

再看下afterNodeAccess方法,发现进行移动节点,到此移动节点的原理我们了解了

  1. void afterNodeAccess(Node<K,V> e) { // move node to last 
  2.    LinkedHashMap.Entry<K,V> last
  3.    if (accessOrder && (last = tail) != e) { 
  4.        LinkedHashMap.Entry<K,V> p =            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;        p.after = null;        if (b == null
  5.            head = a;        else 
  6.            b.after = a;        if (a != null
  7.            a.before = b;        else 
  8.            last = b; 
  9.        if (last == null
  10.            head = p;        else { 
  11.            p.before = last
  12.            last.after = p;        }        tail = p;        ++modCount;    }} 

目前,如果使用 LinkedHashMap 做LRU,还有一个问题困扰着我们,就是如果容量有限,该如何淘汰旧数据?

我们回过头看看 put 方法

  1. public V put(K key, V value) { 
  2.     return putVal(hash(key), key, value, falsetrue); 
  3. final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { 
  4.     Node<K,V>[] tab; Node<K,V> p; int n, i; 
  5.     if ((tab = table) == null || (n = tab.length) == 0) 
  6.         n = (tab = resize()).length; 
  7.     if ((p = tab[i = (n - 1) & hash]) == null
  8.         tab[i] = newNode(hash, key, value, null); 
  9.     else { 
  10.         Node<K,V> e; K k; 
  11.         if (p.hash == hash && 
  12.             ((k = p.key) == key || (key != null && key.equals(k)))) 
  13.             e = p; 
  14.         else if (p instanceof TreeNode) 
  15.             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
  16.         else { 
  17.             for (int binCount = 0; ; ++binCount) { 
  18.                 if ((e = p.next) == null) { 
  19.                     p.next = newNode(hash, key, value, null); 
  20.                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 
  21.                         treeifyBin(tab, hash); 
  22.                     break; 
  23.                 } 
  24.                 if (e.hash == hash && 
  25.                     ((k = e.key) == key || (key != null && key.equals(k)))) 
  26.                     break; 
  27.                 p = e; 
  28.             } 
  29.         } 
  30.         if (e != null) { // existing mapping for key 
  31.             V oldValue = e.value; 
  32.             if (!onlyIfAbsent || oldValue == null
  33.                 e.value = value; 
  34.             afterNodeAccess(e); 
  35.             return oldValue; 
  36.         } 
  37.     } 
  38.     ++modCount; 
  39.     if (++size > threshold) 
  40.         resize(); 
  41.     afterNodeInsertion(evict); 
  42.     return null
  43. void afterNodeInsertion(boolean evict) { // possibly remove eldest 
  44.     LinkedHashMap.Entry<K,V> first
  45.     if (evict && (first = head) != null && removeEldestEntry(first)) { 
  46.         K key = first.key
  47.         removeNode(hash(key), keynullfalsetrue); 
  48.     } 

从put方法中逐步看下来,最终我们发现,如果 removeEldestEntry(first) 方法返回true,则会移除 head,这样就淘汰了最近都没使用的数据。完全符合LRU。

4 最简单的LRU实现

根据上面分析,我们可以如下实现一个最简单的LRU

  1. public class LRUCache<K,V> extends LinkedHashMap<K,V> { 
  2.       private int cacheSize; 
  3.     public LRUCache(int cacheSize) { 
  4.       // 注意:此处需要让 accessOrder = true 
  5.       super(cacheSize, 0.75f, true); 
  6.       this.cacheSize = cacheSize; 
  7.   } 
  8.   /** 
  9.    * 判断元素个数是否超过缓存的容量,超过需要移除 
  10.    */ 
  11.   @Override 
  12.   protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { 
  13.       return size() > cacheSize; 
  14.   } 

 

责任编辑:未丽燕 来源: 今日头条
相关推荐

2022-06-12 06:48:34

2022-06-20 22:37:25

Linux操作系统命令

2022-06-16 17:02:49

微软智能云混合云Azure

2022-06-15 08:21:49

Linux运维工程师

2022-05-27 10:00:06

C++游戏引擎

2022-05-17 09:14:50

聚类算法python

2022-06-15 08:25:07

Python天气数据可视化分析

2022-06-09 18:04:46

网络攻击网络安全

2022-06-06 12:53:17

吴恩达AI机器学习

2022-05-30 14:15:39

人工智能算法数据安全

2022-05-31 10:30:16

元宇宙数字人高清渲染

2022-06-07 11:16:51

云原生人工智能运维

2022-06-24 11:14:00

美团开源

2022-06-28 10:58:35

勒索软件攻击事件

2022-06-28 14:47:43

数据中心服务器科技

2022-06-01 17:47:24

运维监控系统

2022-06-28 10:03:56

CentOSLinux

2022-04-26 06:15:34

降维算法Python

2022-06-09 09:27:16

前端行业生存

2022-06-23 14:03:26

混合ITCIOIT管理工具

同话题下的热门内容

哪个版本的JVM最快?无代码软件发展简史及未来趋势携程基于 GraphQL 的前端 BFF 服务开发实践为什么会存在 1px 问题?怎么解决?EcmaScript 2022 正式发布,有哪些新特性?一文详解|增长那些事儿远程医疗:优势、前景和现有IT解决方案手把手教你实现一个 Python 计时器

编辑推荐

太厉害了,终于有人能把TCP/IP协议讲的明明白白了!牛人5次面试腾讯不成功的经验HBase原理–所有Region切分的细节都在这里了Javascript如何监听页面刷新和关闭事件如何搭建一个HTTPS服务端
我收藏的内容
点赞
收藏

51CTO技术栈公众号