LRU算法
为什么要选双链表+HashMap?
因为,LRU的核心是“最近最少使用”,主要的功能有:
1. 在get数据时,先获取到数据,然后会将该数据放到链表最后位置,方便下次取。(get时可以先到链表尾部取,看是不是)
2. 在put数据时,首先看链表中有没有,有就将这个数据放到链表尾部,方便下次取,没有就在链表尾部插入。
3. 在put数据时,看存入数量满了没,满了就删除链表头部的元素(即删除最老元素)
因此:
使用HashMap的原因:都有查询数据的操作,使用HashMap可以快速查到有没有数据,
使用双向链表的原因:有插入与删除的操作,选用Linked双向链表更有优势。
代码:
package Algorithm.LRU; import java.util.HashMap; import java.util.Map; /** * LRU(Least Recently Used)即“最近最少使用”,这里使用双链表+hashMap实现了LRU算法 * 注意:LRUCache中没有重复的节点 * * @author chenjunjie * @since 2018-04-25 */ public class LRUCache { Node head, tail; int size; int capacity; Map<Integer,Node> map; /** * 定义内部类,Node节点 */ static class Node { int key; int val; Node pre; Node post; /** * 构造函数 * @param key Map的key * @param val May的value */ Node(int key, int val) { this.key = key; this.val = val; } } /** * 构造函数,初始化 * @param capacity */ public LRUCache(int capacity) { this.head = null; this.tail = null; this.size = 0; this.capacity = capacity; // 使用了HashMap // 注意这里Map中的value为Node类型,这样可以方便索引到某个具体node this.map = new HashMap<Integer, Node>(); } /** * 获取最近使用node * 先remove掉节点以前所处位置,然后添加到链表的尾部,从而达到最近使用的效果 * @param key * @return 返回value */ public int get(int key) { if(!map.containsKey(key)) { return -1; } Node temp = map.get(key); remove(temp); add(temp); return temp.val; } /** * 添加新节点,并保证放入最后节点 * @param key map的key值 * @param value map中的value */ public void put(int key, int value) { Node temp; // 如果之前有key,但不是放在链表尾部,则先删除然后添加到尾部 if(map.containsKey(key)){ temp = map.get(key); remove(temp); map.remove(key); size--; } // 如果超过了容量,这删除头部节点,即删除“最老”节点 if(size == capacity){ Node h = head; remove(h); map.remove(h.key); size--; } temp = new Node(key,value); // 链表中添加Node add(temp); // HashMap添加Entry map.put(key,temp); size++; } /** * 普通的linked链表删除操作 * @param n 要删除的节点 */ private void remove(Node n) { // 如果只有一个节点 if(n==head && n==tail){ head = null; tail = null; } // 如果是头节点 else if(n == head) { head = head.post; head.pre = null; } // 如果是尾节点 else if(n == tail) { tail = tail.pre; tail.post = null; } // else{ n.pre.post = n.post; n.post.pre = n.pre; } n.pre = null; n.post = null; } /** * 在链表尾部加入节点 * @param n 添加节点 */ private void add(Node n) { if(head == null && tail == null) { head = n; tail = n; }else { tail.post = n; n.pre = tail; tail = n; } } /** * getMap,方便测试遍历查询结果 * @return */ public Map<Integer, Node> getMap() { return map; } /** * getHead,方便测试遍历查询结果,实际上不要暴露出来 * @return */ public Node getHead(){ return head; } }
测试:
public static void main (String[] args) { LRUCache lurCache = new LRUCache(10); int N = 12 ; //7 int i = 1; while (i<N){ lurCache.put(i,i*2); i++; } System.out.println(lurCache.get(4)); lurCache.put(4,666); // 遍历map Map hsMap = lurCache.getMap(); Iterator it = hsMap.entrySet().iterator(); while(it.hasNext()){ Map.Entry<Integer,LRUCache.Node> result = (Map.Entry<Integer,LRUCache.Node>)it.next(); System.out.printf("(key=%d,value=%d)\n",result.getKey(),result.getValue().val); } System.out.println("-----------"); // 遍历node LRUCache.Node n = lurCache.getHead(); LRUCache.Node cur = n; while(cur != null){ System.out.printf("(key=%d,value=%d)\n",cur.key,cur.val); cur = cur.post; } }
结果如下:
特性:
1. 插入了12条数据,由于定义的容量为10,因此每次满容量时删除头结点的数据。
2. 修改key=4的值,修改后放到队列尾部
3. 使用get()后,node节点的数据也会放到队列尾部(ps:测试代码中没有写)
扩展(待续):
/**
* LRU是Least Recently Used 的缩写,即“最近最少使用”
* 扩展 LinkedHashMap 可以实现就实现了LRU,
* LinkedHashMap中最近读取的会放在最前面,最最不常读取的会放在最后,同时
* 调用removeEldestEntry会移除链表中“最老”的节点,但注意源码中调用该方法返回的值为false
* 因此,可以继承LinkedHashMap重写removeEldestEntry, 从而构建LRU缓存
*/