1. LRU缓存机制

1.自己构建

力扣 146.LRU缓存机制, 题解参考资料 Dong哥狗哥, 并转载了他们的图片

计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢?

LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。

题目要求:
设计一种缓存结构,该结构在构造时确定大小,假设大小为K,并有两个功能:set(key,value):将记录(key,value)插入该结构。get(key):返回key对应的value值。
【要求】
set和get方法的时间复杂度为O(1)。
某个key的set或get操作一旦发生,认为这个key的记录成了最经常使用的。
当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的。
【举例】
假设缓存结构的实例是cache,大小为3,并依次发生如下行为:
cache.set(“A”,1)。最经常使用的记录为(“A”,1)。
cache.set(“B”,2)。最经常使用的记录为(“B”,2),(“A”,1)变为最不经常的。
cache.set(“C”,3)。最经常使用的记录为(“C”,2),(“A”,1)还是最不经常的。
cache.get(“A”)。最经常使用的记录为(“A”,1),(“B”,2)变为最不经常的。
cache.set(“D”,4)。大小超过了3,所以移除此时最不经常使用的记录(“B”,2),加入记录 (“D”,4),并且为最经常使用的记录,然后(“C”,2)变为最不经常使用的记录

题解:
那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap。

LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:
linkedHashMap1

在哈希表中value存放的是节点的引用,通过key可以直接查询到某个节点,也可以直接在链表中定位!

linkedHashMap2

1.设计好的Cache的上层结构

public static class MyCache<K>{
    // cache缓存包括,哈希表,双端链表 和 容量
    private HashMap<K, Node<K>> keyNodeMap;
    private NodeDoubleLinkedList<K> nodeList;
    private final int capacity;
    // 初始化结构体
    public MyCache(int capacity) {
        this.capacity = capacity;
        this.keyNodeMap = new HashMap<K, Node<K>>();
        this.nodeList = new NodeDoubleLinkedList<K>();
    }
    // get(key):返回key对应的value值。
    public int get(K key){
        // 如果这个key还在缓存中
        if (keyNodeMap.containsKey(key)){
            int val = keyNodeMap.get(key).value;
            // 这个操作,会更新被操作的节点的顺序
            nodeList.moveNodeToTail(keyNodeMap.get(key));
            return val;
        }else{
            // 没有找到,它被移除了
            return -1;
        }
    }

    //set(key,value):将记录(key,value)插入该结构。
    public void put(K key, int value){
        // 可能会是更新,也可能是插入新的点
        // 更新操作不会移除点
        if (keyNodeMap.containsKey(key)){
            Node<K> node = keyNodeMap.get(key);
            node.value = value;
            // 这个操作,会更新被操作的节点的顺序
            nodeList.moveNodeToTail(node);
        }else {// 插入新节点了,要先判断容量够么
            if (capacity == keyNodeMap.size()){
                // 也可以定义一个函数: removeMostUnusedCache()
                // 从链表中去除
                Node<K> node = nodeList.removeHead();
                // 在HashMap表中也清除它
                keyNodeMap.remove(node.key);

            }
            // 插入新的点
            Node<K> newNode = new Node<K>(key, value);
            keyNodeMap.put(key, newNode);
            nodeList.addNode(newNode);
        }
    }
}

2.实现其中的双端链表 和 基本的node类

// 定义包含 <key,value> 的Node节点
public static class Node<K>{
    public K key;
    public int value;
    public Node<K> pre;
    public Node<K> next;

    public Node(K key, int value){
        this.key = key;
        this.value = value;
        pre = null;
        next = null;
    }
}

// 在双端链表中要实现,三个功能
// void addNode(Node<K> node) : 添加新的节点,并放到链表尾部
// void moveNodeToTail(Node<K> node) : 把node放到链表尾部
// Node<K> removeHead() : 把头部节点从链表上移除,并返回它
public static class NodeDoubleLinkedList<K>{
    // 设置头尾指针
    public Node<K> head;
    public Node<K> tail;
    // 结构体初始化
    public NodeDoubleLinkedList(){
        head = null;
        tail = null;
    }
    // 添加新的节点,并放到链表尾部
    // 要分情况: 是空链表么
    public void addNode(Node<K> node){
        // 空链表
        if (head == null){
            head = node;
            tail = node;
        }else { // 非空链表
            this.tail.next = node;
            node.pre = this.tail;
            this.tail = node;
        }
    }
    // node 首先一定会存在
    public void moveNodeToTail(Node<K> node){
        // 本来就是尾巴
        if (node == tail){
            return;
        }
        // 本来是头部,就会给链表换头了
        if (node == head){
            node.next.pre = null;
            // 更换头节点
            head = node.next;
            node.next = null;
            // 拼接到尾节点上, 代码重复
        }else { // 中间节点
            // 连接好左右两边
            node.pre.next = node.next;
            node.next.pre = node.pre;
            node.next = null;
            node.pre = null;
            // 拼接到尾节点上, 代码重复
        }
        // 把node放在tail上
        tail.next = node;
        node.pre = tail;
        tail = node;
    }

    // 移除头节点,并把它返回
    public Node<K> removeHead(){
        // 压根没有头
        if (head == null){
            return null;
        }
        // 记录原来的头节点
        Node<K> res = head;
        // 只有一个节点
        if (head == tail){
            head = null;
            tail = null;
        }else{
            // 把head标志位,向后移动一位
            head = res.next;
            // 根据新的头节点,把以前的头节点左右设置为空
            res.next = null;
            // 彻底断开原来的头节点, 新头部pre置空
            head.pre = null;
        }
        return res;
    }
}

2.LinkedHashMap build-in

参考资料
LinkedHashMap保持HashMap的数据结构,但是自己也维护一个双向循环链表。

LinkedHashMap内部有accessOrder标记,为false时,双向链表按插入的顺序排列。因为新插入的元素都是尾插法,此时我们需要自己实现makeItAsRecentlyUsed()方法,其实就是删除它在插入。

accessOrder=true时,每次对元素进行增删改查,都会将该元素放到链表尾部。使这个链表将最新操作的元素放入链表尾,长时间未使用的放入头部。这种即是LRU算法。

1.accessOrder=false

public static class MyCache{
    int capacity;
    LinkedHashMap<Integer, Integer> linkedHashMap;

    // 结构体初始化
    public MyCache(int capacity){
        this.capacity = capacity;
        this.linkedHashMap = new LinkedHashMap<>();
    }

    public int get(int key){
        if (linkedHashMap.containsKey(key)){
            int val = linkedHashMap.get(key);
            // 因为get操作,把这个它更新了
            makeItAsRecentlyUsed(key);
            // 查询到了结果
            return val;
        }
        // 没有这个点
        return -1;
    }

    public void put(int key, int value){
        // 更新操作
        if (linkedHashMap.containsKey(key)){
            // map上的更新
            linkedHashMap.put(key, value);
            makeItAsRecentlyUsed(key);
        }else {
            if (linkedHashMap.size() >= capacity){
                // 移除链表的头部
                int oldestKey = linkedHashMap.keySet().iterator().next();
                linkedHashMap.remove(oldestKey);
            }
            // 腾出空间,添加新的节点
            linkedHashMap.put(key, value);
        }
    }

    private void makeItAsRecentlyUsed(int key){
        int value = linkedHashMap.get(key);
        linkedHashMap.remove(key);
        // 重新加入,就自动放到tail上了
        linkedHashMap.put(key, value);
    }

}

2.accessOrder=true

public static class MyCache{
    int capacity;
    LinkedHashMap<Integer, Integer> linkedHashMap;

    // 结构体初始化
    public MyCache(int capacity){
        this.capacity = capacity;
        // 0.65f是扩容因子,true是启用accessOrder
        this.linkedHashMap = new LinkedHashMap<>(capacity, 0.65f, true);
    }

    public int get(int key){
        if (linkedHashMap.containsKey(key)){
            int val = linkedHashMap.get(key);
            // 因为get操作,把这个它更新了
            // makeItAsRecentlyUsed(key);
            // 查询到了结果
            return val;
        }
        // 没有这个点
        return -1;
    }

    public void put(int key, int value){
        // 更新操作
        if (linkedHashMap.containsKey(key)){
            // map上的更新
            linkedHashMap.put(key, value);
            // makeItAsRecentlyUsed(key);
        }else {
            if (linkedHashMap.size() >= capacity){
                // 移除链表的头部
                int oldestKey = linkedHashMap.keySet().iterator().next();
                linkedHashMap.remove(oldestKey);
            }
            // 腾出空间,添加新的节点
            linkedHashMap.put(key, value);
        }
    }
}