前言
对于LRU缓存,
记忆点:hash + 双向链表;
逻辑点:线性表 -> 单向链表 -> hash定位 -> 双向链表 -> 头尾节点。
一、LRU缓存
二、问题分析
1、记忆点
对于LRU来说,记住逻辑根,即可以此为线索去分析问题。
逻辑根: 带头节点的双向链表 + HashMap;
2、 问题分析
逻辑结构)存储一些有序元素,所以选择线性表作为逻辑结构。
物理结构)由于需要删除最近最久未使用的数据,即删除尾元素,所以选择单向链表作为物理结构。
O(1)复杂度get(K)&put(K,V))由于需要O(1)快速get / put,易想到hash进行key / Node映射。
双向链表)由于hash定位到删除节点,需要拿到前向节点才能快速删除,所以将单向链表改为双向链表。
头尾节点)经典头节点,统一链表为空的操作,也方便头尾节点的操作。
3、其他注意细节
1)为保证HashMap不扩容,可提前设置容量大小。而扩容的标准是loadfactor是否超过,其默认值为0.75。
2)为了LRU的抽象性,可以构造类为LRU<K,V>
,Node类中也可设置为K key;V val;
3)为了代码的复用性,将常见操作独立出来,比如删除指定节点即断链unlink(Node)
,删除尾节点deleteTail()
,添加节点到头部appendHead()
.
4)注意链表节点数据,与map中的数据同步,比如删除尾元素时需要map.remove(K key)
,添加元素时map.put(K,Node)
.
5)为了减少gc压力,即存活对象的标记,应在代码中把无用节点的pre / next指针先断掉。
6)当LRU出现并发时,链表&map本身就有并发问题,而且两者数据一致性也有并发问题,并发LRU,可采用并发安全的链表&Map。除此之外,还需要用同步代码块将两者的操作变为原子操作。
三、源码
class LRUCache {
private final double loadFactor = 0.75;
private int capacity;
Map<Integer,Node> fx;
Node dummyHead,dummyTail;
public LRUCache(int capacity) {
// 初始化LRU容量
this.capacity = capacity;
// 初始化map
int cap = (int)((capacity + 1) / loadFactor) + 1;
fx = new HashMap<>(cap);// 不超过loadfactor,不扩容。
// 初始化链表。
dummyHead = new Node();
dummyTail = new Node();
dummyHead.next = dummyTail;
dummyTail.pre = dummyHead;
}
public int get(int key) {
if(!fx.containsKey(key)) return -1;
Node n = fx.get(key);
unlink(n);
appendHead(n);
return n.val;
}
public void put(int key, int value) {
if(fx.containsKey(key)) unlink(fx.get(key));
Node node = new Node(key,value);
appendHead(node);
fx.put(key,node);// map保持数据同步
// 超载情况,需要删除尾节点,并进行map数据同步。
if(fx.size() > capacity) fx.remove(deleteTail());
}
// 断链
public void unlink(Node cur){
Node front = cur.pre;
Node behind = cur.next;
front.next = behind;
behind.pre = front;
// 给gc减少压力
cur.next = cur.pre = null;
}
// 在链表尾部删除节点
public int deleteTail(){
Node tail = dummyTail.pre;
Node front = tail.pre;
Node behind = tail.next;
front.next = behind;
behind.pre = front;
// 减少gc压力
tail.next = tail.pre = null;
return tail.key;
}
// 在链表头部添加节点
public void appendHead(Node cur){
Node front = dummyHead;
Node behind = dummyHead.next;
front.next = cur;
behind.pre = cur;
cur.pre = front;
cur.next = behind;
}
// 双向链表结构
static class Node{
int key,val;
Node next,pre;
public Node(){}
public Node(int key,int val){
this.key = key;
this.val = val;
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
总结
1)总结一个知识点的逻辑根,辅助自己将来碰到同样的题进行快速分析。如LRU回忆逻辑根hash+双向链表,再分析时,可从线性表 -> 链表 -> hash定位 -> 双向链表 -> 头尾节点。
2)函数不仅解决复用性的同时,还可将逻辑宏观清晰化。除此之外,多借鉴大佬/巨人的写法,才有所长进。
参考文献
[1] LeetCode LRU缓存
[2] CyC2018 github