前几天在 LeetCode 上做题时做到了与 LRU 和 LFU 相关的题目,恰逢现在是暑期实习面试的高峰,LRU 也是面试中的常考内容,所以在此对 LRU 和 LFU 的 Java 实现做一个总结。
LRU 最近最久未使用
简单介绍一下 LRU,此处搬运自百度百科:
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
顾名思义,LRU 是一种淘汰算法,具体的淘汰策略是把最近最久未使用(或者说是最近最少使用)的页面淘汰掉,LRU 算法在操作系统的内存置换,Redis 缓存的数据淘汰策略中都有所涉及。百度百科中的介绍提到了根据 t 值进行淘汰,但是在实际应用中保存 t 值并不是一件容易的事,所以我们需要借助特别的数据结构来实现 LRU 算法。
具体来说,使用一个 HashMap 和一个自定义的双向链表:链表节点存储key,value,前向指针和后向指针;HashMap 中存储 key 和对应的链表节点;双向链表代表使用顺序,越靠近头节点表示最近使用过,越靠近尾节点表示很久没有使用,作用就是模拟具体的淘汰步骤了。
双向链表节点的结构如下
class ListNode { int key; int val; ListNode prev; ListNode next; }
LRU 的组成,我们定义 head 和 tail 作为辅助节点,表示双向链表的首尾
class LRUCache { Map<Integer, ListNode> cache; ListNode head; ListNode tail; int maxCap; public LRUCache(int capacity) { cache = new HashMap<>(capacity); head = new ListNode(); tail = new ListNode(); head.next = tail; tail.prev = head; maxCap = capacity; } }
具体的操作步骤
- 如果 put 进来的 key 不存在,则在 HashMap 中进行记录并插入到链表中
- 如果 put 进来的 key 已经存在,则通过 HashMap 找到对应的链表节点,修改其 value 值。接下来,就是 LRU 算法的关键部分,要把该节点从链表上取下来,并插入到链表的头部,表示该节点最近被使用过
- 如果 put 时已达到最大容量,则从链表的尾部删除一个节点(因为尾部的节点是很久没有使用过的),同时删除 HashMap 中的记录项
public void put(int key, int value) { if (cache.containsKey(key)) { // 存在 // 获取到 key 对应的节点 ListNode node = cache.get(key); // 修改 value 值 node.val = value; // 把该节点从链表上取下来 node.prev.next = node.next; node.next.prev = node.prev; // 插入到链表头部 addToHead(node); } else { // 不存在 // 创建一个节点 ListNode node = new ListNode(); node.key = key; node.val = value; // 判断是否还有空间 if (cache.size() == maxCap && maxCap != 0) { // 已达到最大容量,移除最近最久未使用 ListNode popNode = popTail(); cache.remove(popNode.key); } // 插入到链表头部 addToHead(node); // 在 map 中进行记录 cache.put(key, node); } }
- get 方法也是同理,需要把节点从链表上取下来,并插入到链表的头部,表示该节点最近被使用过
public int get(int key) { if (cache.containsKey(key)) { // 获取到 key 对应的节点 ListNode node = cache.get(key); // 把该节点从链表上取下来 node.prev.next = node.next; node.next.prev = node.prev; // 插入到链表头部 addToHead(node); return node.val; } return -1; }
完整的代码如下:
class LRUCache { class ListNode { int key; int val; ListNode prev; ListNode next; } Map<Integer, ListNode> cache; ListNode head; ListNode tail; int maxCap; public LRUCache(int capacity) { cache = new HashMap<>(capacity); head = new ListNode(); tail = new ListNode(); head.next = tail; tail.prev = head; maxCap = capacity; } public void addToHead(ListNode node) { head.next.prev = node; node.next = head.next; head.next = node; node.prev = head; } public ListNode popTail() { ListNode tmp = tail.prev; tmp.prev.next = tail; tail.prev = tmp.prev; return tmp; } public int get(int key) { if (cache.containsKey(key)) { ListNode node = cache.get(key); node.prev.next = node.next; node.next.prev = node.prev; addToHead(node); return node.val; } return -1; } public void put(int key, int value) { if (cache.containsKey(key)) { ListNode node = cache.get(key); node.val = value; node.prev.next = node.next; node.next.prev = node.prev; addToHead(node); } else { ListNode node = new ListNode(); node.key = key; node.val = value; if (cache.size() == maxCap && maxCap != 0) { ListNode popNode = popTail(); cache.remove(popNode.key); } addToHead(node); cache.put(key, node); } } }
LFU 最不经常使用