LRU缓存[线性表 -> 链表 -> hash定位 -> 双向链表]

前言

对于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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值