LRU缓存

146.LRU缓存篇

问题描述

实现一个满足LRU(最近最少使用)缓存约束的LRUCache类。该缓存满足以下三个条件:

  1. 缓存容量LRUCache(int capacity)
  2. int get(int key) 如果关键字key存在,则返回对应value,否则返回-1;
  3. void put(int key, int value) 如果关键字存在,更新value的值;如果不存在,则向缓存中插入新键值对,如果插入导致超过缓存容量,则删除最近最少使用的关键字的键值对。
    要求:get 与 put要以O(1)实现

思路:

  1. get与put要以O(1)实现 => 数组
  2. 键值对 => map
    1+2 = HashMap(底层就是数组加链表加红黑树)
    但是在put新键值对时,要删除最近最少使用的键值对,同时put要以O(1)实现,这就要求我们在存储数据时要求它有序存储,实现O(1)复杂度找到要删除的节点,这里很自然过渡到了LinkedHashMap,因为他就是在HashMap的基础上维护了双向链表来实现有序存储。

问题解决

  1. 方法1:直接利用LinkedHashMap来解决,这要求了解其源码
class LRUCache extends LinkedHashMap<Integer, Integer> {
    private static final float DEFAULT_LOAD_FACTOR = 0.75f; //扩容因子,当实际容量与最大容量比值大于扩容因子时进行扩容。
    private final int capacity;//容量

    public LRUCache(int capacity) {
        super(capacity, DEFAULT_LOAD_FACTOR, true);//这里的true是初始化accessOrder为true:即按照读取顺序来排序,默认是false:插入顺序排序
        this.capacity = capacity;//容量
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);//这里调用的getOrDefault就是去判断是否有这个key,没有就返回-1
        
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;//指定当map的容量大于指定容量时执行删除最老的元素(这个最老就是最近最少使用:通过指定accessOrder实现)
    }
}

public V getOrDefault(Object key, V defaultValue) {
       Node<K,V> e;
       if ((e = getNode(key)) == null)
           return defaultValue;
       if (accessOrder)
           afterNodeAccess(e);//调用的这个函数是执行读取操作后的回调函数,里面实现了将读的元素放到末尾
       return e.value;
   }

  1. 方法2:自己通过双向链表与HashMap来实现,其中HashMap为key to node的映射
    class LRUCache {
    private static class LinkedListNode {
         int key, value; //键值对
         LinkedListNode pre, next; //双向链表
         public LinkedListNode(int key, int value) {
             this.key = key;
             this.value = value;
         }
     }
     //利用dummy简化代码逻辑,将双向链表转换为双向循环链表,减少判空
     private final LinkedListNode dummy = new LinkedListNode(0, 0);
     //缓存的容量
     private final int capacity;
     //存储key到节点的映射
     private final Map<Integer, LinkedListNode> hashmap = new HashMap<>();
    
     public LRUCache(int capacity) {
         //初始化
         this.capacity = capacity;
         //一开始就让他自循环,就不用再插入,移动操作中判空了,没有dummy节点的话可以参考LinkedHashMap的源码,定义一个指针head指向头部,一个指针指向尾部tail
         dummy.next = dummy;
         dummy.pre = dummy;
     }
    
     public int get(int key) {
    
         if (hashmap.containsKey(key)) {
             //表中已经存在关键字,除了取之外还要调整他在链表中的位置
             LinkedListNode node = hashmap.get(key);
             //删除该节点重新从头插入,以下都是通过这样实现位置排序的
             delete(node);
             insert(node);
             return node.value;
         } else {
             //表中没有关键字
             return -1;
         }
     }
    
     public void put(int key, int value) {
         if (hashmap.containsKey(key)) {
             //表中存在,更改值,并更新链表
             LinkedListNode node = hashmap.get(key);
             node.value = value;
             delete(node);
             insert(node);
         } else {
             //表中不存在,插入key-value,插入时判断是否超过了容量
             LinkedListNode newNode = new LinkedListNode(key, value);
             if (hashmap.size() == capacity) {
                 //需要删除最近少使用的节点:尾巴节点,删除一个节点包括删除他的hash表中的键值对
                 LinkedListNode node = dummy.pre;
                 hashmap.remove(node.key);
                 delete(node);
                 //插入新节点
                 insert(newNode);
                 hashmap.put(key, newNode);
             } else {
                 //直接插入
                 hashmap.put(key, newNode);
                 //从头插入链表
                 insert(newNode);
             }
         }
     }
    
     private void delete(LinkedListNode node) {
         node.pre.next = node.next;
         node.next.pre = node.pre;
     }
    
     private void insert(LinkedListNode node) {
         node.pre = dummy;
         node.next = dummy.next;
         node.pre.next = node;
         node.next.pre = node;
     }
    
    

}

tips:

  1. 涉及到链表与hash表两个我们维护的表时,也就是多表需要程序员维护时,想清楚什么时候要操作什么表。
  2. dummy node 这里的作用是简化代码逻辑,dummy node还可以应用头节点改变时,找到头节点,dummy.next
  3. 通过源码阅读,了解了怎么优雅的操作双向链表的插入,删除。
posted @ 2025-07-03 00:30  waterme  阅读(15)  评论(0)    收藏  举报