LRU算法介绍
LRU的全称为Least Recently Used,翻译出来就是最近最少使用的意思,它是一种内存淘汰算法,当内存不够时,将内存中最久没使用的数据清理掉。
LUR算法是内存管理的一种页面置换算法,就是用来删除内存中不被使用的数据,腾出空间来把常用的数据存进去。
为什么需要LRU算法?
现代计算机内存仍是相当昂贵的,那么如果利用好管理好有限的内存,来为用户提供更好的性能,是一个有意义的议题。
LRU(Least Recently Used) 即最近最少使用,属于典型的内存淘汰机制。
通俗的说,LRU算法认为,最近被频繁访问的数据会具备更高的留存,淘汰那些不常被访问的数据,这样可以极大的节省内存的使用。
所以,LRU算法也就常用于缓存的淘汰策略,更好的提升内存的使用率。
LRU算法原理
LRU算法的实现原理:把所有的缓存数据存入链表中,新插入的或被访问的数据存入链表头,如果链表满了,就把尾部的数据清除。
如下图所示:
为了更好地描述和理解LRU算法的原理,此处我们以一个队列举例,队列的头部表示最久没有被访问的数据,队列的尾部表示最近刚访问的数据。
假如定义一个长度为4的队列,我们随意添加几个数据进去,如下图:
此时我们访问一次下标为1的元素,根据LRU算法的思想,需要将下标为1的元素移动到队列的尾部,表示该元素最近访问过,后面的元素依次前移,如下图:
这样,每次队列满了的时候添加新的元素,我们只需要将队列头部的元素移除掉即可,这样就达到了淘汰最近最少使用数据的功能,这就是LRU算法的基本原理。
这样,每次队列满了的时候添加新的元素,我们只需要将队列头部的元素移除掉即可,这样就达到了淘汰最近最少使用数据的功能,这就是LRU算法的基本原理。
LRU算法实现
上面为了方便理解,我们使用的队列来举的例子,虽然可以实现LRU算法,但性能并不高,因为每次更新队列元素之后,都要对队列里面的元素进行大量拷贝(移动),并且访问队列元素时的时间复杂度是O(n),所以,如果要实现LRU算法,需要再对上面的模型进行优化。
首先,针对元素更新后需要大量拷贝数据的问题,我们可以使用双向链表这种数据结构来解决,因为双向链表中,每个节点都维护有自己的前置节点和后置节点的信息,当我们需要移动一个元素到其他位置时,只需要更新几个节点的前置节点和后置节点信息即可,其他没有涉及的节点数据不需更新,如下图:
然后,针对查询队列元素性能较差的问题,我们可以使用key-value数据结构来解决,因为key-value的查询时间复杂度是O(1),此处我们使用HashMap来实现。实现原理图如下:
双向链表中维护的是我们实际的数据,HashMap中维护的是双向链表中每个节点的key和node之间的关系,这样,我们在查询数据的时候,可以直接根据HashMap中的key找到对应的双向链表中的节点,然后得到节点中的值;并且在更新链表数据时,也只需要更新涉及节点的指向关系即可。
LRU算法实现
LRU的简易代码实现,如下:
public class LRUCache { private HashMap<String, LRUNode> map; private int capacity; private LRUNode head; private LRUNode tail; public void set(String key, Object value) { LRUNode node = map.get(key); if (node != null) { node = map.get(key); node.value = value; remove(node, false); } else { node = new LRUNode(key, value); if (map.size() >= capacity) { // 每次容量不足时先删除最久未使用的元素 remove(tail, true); } map.put(key, node); } // 将刚添加的元素设置为head setHead(node); } public Object get(String key) { LRUNode node = map.get(key); if (node != null) { // 将刚操作的元素放到head remove(node, false); setHead(node); return node.value; } return null; } private void setHead(LRUNode node) { // 先从链表中删除该元素 if (head != null) { node.next = head; head.prev = node; } head = node; if (tail == null) { tail = node; } } // 从链表中删除此Node,此时要注意该Node是head或者是tail的情形 private void remove(LRUNode node, boolean flag) { if (node.prev != null) { node.prev.next = node.next; } else { head = node.next; } if (node.next != null) { node.next.prev = node.prev; } else { tail = node.prev; } node.next = null; node.prev = null; if (flag) { map.remove(node.key); } } public LRUCache(int capacity) { this.capacity = capacity; this.map = new HashMap<String, LRUNode>(); } }
陈睿mikechen
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》