一、LRU算法介绍
LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,为虚拟页式存储管理服务。
LRU算法的提出,是基于这样一个事实:在前面几条指令中使用频繁的页面很可能在后面的几条指令中频繁使用。反过来说,已经很久没有使用的页面很可能在未来较长的一段时间内不会被用到。
这个,就是著名的局部性原理。此外,LRU算法也经常被用作缓存淘汰策略。
二、实现方式
最常见的实现是使用一个双向链表保存缓存数据,详细算法实现如下:
1、新数据插入到链表头部;
2、每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3、当链表满的时候,将链表尾部的数据丢弃。
优点:实现简单。
缺点:每次访问都需要更新链表,因此代价略高。
三、代码实现
1、版本一:
/** * 在版本一中,利用HashMap和一个简单的双向链表来实现LRU缓存 */ class LRUCache { // 双向链表节点定义 class Node { int key; int val; Node prev; Node next; } private int capacity; //保存链表的头节点和尾节点 private Node first; private Node last; private Map<Integer, Node> map; public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(capacity); } public int get(int key) { Node node = map.get(key); //为空返回-1 if (node == null) { return -1; } moveToHead(node); return node.val; } private void moveToHead(Node node) { if (node == first) { return; } else if (node == last) { last.prev.next = null; last = last.prev; } else { node.prev.next = node.next; node.next.prev = node.prev; } node.prev = first.prev; node.next = first; first.prev = node; first = node; } public void put(int key, int value) { Node node = map.get(key); if (node == null) { node = new Node(); node.key = key; node.val = value; if(map.size() == capacity) { removeLast(); } addToHead(node); map.put(key, node); } else { node.val = value; moveToHead(node); } } private void addToHead(Node node) { if (map.isEmpty()) { first = node; last = node; } else { node.next = first; first.prev = node; first = node; } } private void removeLast() { map.remove(last.key); Node prevNode = last.prev; if (prevNode != null) { prevNode.next = null; last = prevNode; } } @Override public String toString() { return map.keySet().toString(); } public static void main(String[] args) { LRUCache cache = new LRUCache(3); cache.put(1, 1); cache.put(2, 2); cache.put(3, 3); cache.get(1); cache.put(4, 3); System.out.println(cache); } }
打印结果: [4, 1, 3]。
可以看到这种实现方式 get 和 put 的操作时间复杂度都是O(1)的。
2、版本二:
其实JDK已经提供了一个基于HashMap和双向链表实现的数据结构,它就是LinkedHashMap,它内部维护的双向链表,可以帮助维护两种顺序:插入顺序和LRU顺序。
public class LRUCache<K,V> extends LinkedHashMap<K, V>{ //首先设定最大缓存空间 MAX_ENTRIES 为 3; private static final int MAX_ENTRIES = 3; //之后使用LinkedHashMap的构造函数将 accessOrder设置为 true,开启 LRU顺序; public LRUCache() { super(MAX_ENTRIES, 0.75f, true); } //最后覆盖removeEldestEntry()方法实现,在节点多于 MAX_ENTRIES 就会将最近最少使用的数据移除。 //因为这个函数默认返回false,不重写的话缓存爆了的时候无法删除最近最久未使用的节点 @Override protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) { //在容量超过最大允许节点数的时候返回true,使得在afterNodeInsertion函数中能执行removeNode() return size() > MAX_ENTRIES; } public static void main(String[] args) { LRUCache<Integer, Integer> cache = new LRUCache<>(); cache.put(1, 1); cache.put(2, 2); cache.put(3, 3); cache.get(1); cache.put(4, 4); System.out.println(cache.keySet()); } }
打印结果:[3, 1, 4]。