Java 数据结构第三十期:LRUCache 的“遗忘机制”

专栏:Java数据结构秘籍

个人主页:手握风云

目录

[一、LRU Cache 的概念](#一、LRU Cache 的概念)

[二、LRU Cache 的实现](#二、LRU Cache 的实现)

[三、JDK 中类似 LRUCahe 的数据结构 LinkedHashMap](#三、JDK 中类似 LRUCahe 的数据结构 LinkedHashMap)

[四、LRU Cache 中的 OJ](#四、LRU Cache 中的 OJ)

[4.1. LRU 缓存](#4.1. LRU 缓存)


一、LRU Cache 的概念

LRU Cache 是 Least Recently Used 的缩写,意为"最近最少使用",它是一种经典的 Cache(缓存)替换算法 。广义上的 Cache 指的是位于速度差异较大的两种硬件之间(例如 CPU 与主存、内存与硬盘、甚至硬盘与网络之间)用于协调数据传输速度差异的结构 。由于 Cache 的容量是有限的,当存储空间耗尽且又有新内容需要添加时,系统就必须依据特定的策略挑选并舍弃原有的部分内容,以腾出空间来存放新的数据 。

LRU 算法的核心替换原则是将最近一段时间内"最久没有使用过"的内容替换掉 。虽然字面意思是"最近最少使用",但将其理解为"最久未使用"往往更为形象,因为该算法的判断标准是基于最后一次访问的时间远近,优先淘汰那些长时间未被访问的数据 。

二、LRU Cache 的实现

在数据结构实现方面,为了保持高效并实现 时间复杂度的 `put`(存入)和 `get`(读取)操作,经典的 LRU Cache 实现通常采用**双向链表**搭配**哈希表**的组合 。使用哈希表是因为其增删查改的时间复杂度为 ,可以快速定位数据;而使用双向链表则是因为它支持在任意位置进行 的插入和删除,适合动态维护数据的访问顺序 。在这种结构中,数据被访问或新插入后通常会被移动到链表的尾部(代表最近被使用),而当缓存容量达到上限时,链表头部(代表最久未使用)的数据则会被优先移除 。

三、JDK 中类似 LRUCahe 的数据结构 LinkedHashMap

Java 中的 LinkedHashMap 是一个继承自 HashMap 并实现 Map 接口的类,它是一个有序的哈希表,能够根据插入顺序或访问顺序维护键值对的顺序。LinkedHashMap 的主要特性包括:1.LinkedHashMap 会记住元素的插入顺序,并按照插入顺序进行迭代;2.LinkedHashMap 提供了一种访问顺序模式,可以根据访问顺序而不是插入顺序进行迭代。默认情况下,LinkedHashMap 使用插入顺序模式,但如果在构造时将 accessOrder 参数设为 true,则会启用访问顺序模式。在这种模式下,每次调用 get() 或 put() 方法访问已存在的键时,对应的节点会被移动到链表尾部,使最近访问的元素始终位于最后。

java 复制代码
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
java 复制代码
// 指定初始容量和负载因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

// 指定初始容量的构造方法。允许自定义初始容量,其他参数使用默认值
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

// 使用默认的访问顺序(插入顺序),初始容量为16,负载因子为0.75
public LinkedHashMap() {
    super();
    accessOrder = false;
}

public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

// 指定初始容量、负载因子和访问顺序
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
java 复制代码
package lru;

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map =
                new LinkedHashMap<>(16, 0.7f, true);
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);
        map.put("four", 4);

        System.out.println(map);
        System.out.println("获取元素:" + map.get("two"));
        System.out.println(map);
    }
}

如果访问顺序标志设为 true,则按照 LRU 算法进行排列,最近最少使用的值靠前。

如果没有指定访问顺序标志,默认为 false,只按照插入顺序排列。

java 复制代码
package lru;

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map =
                new LinkedHashMap<>(16, 0.7f);
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);
        map.put("four", 4);

        System.out.println(map);
        System.out.println("获取元素:" + map.get("two"));
        System.out.println(map);
    }
}

四、LRU Cache 中的 OJ

4.1. LRU 缓存

第一种解法:我们可以通过继承 LinkedHashMap 类,通过重写里面的 get()、removeEldestEntry()、put() 3个方法,从而实现 LRU 算法。

java 复制代码
class LRUCache extends LinkedHashMap<Integer, Integer> {

    private int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    public Integer get(Object key) {
        // 调用父类的 getOrDefault() 方法,如果不存在对应的 key,返回-1
        return super.getOrDefault(key, -1);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        // 如果当前大小超过容量,返回 true,表示需要移除最老的条目
        return size() > capacity;
    }

    @Override
    public Integer put(Integer key, Integer value) {
        return super.put(key, value);
    }
}

第二种解法:我们利用自己创建的双向链表与集合框架中的 HashMap 来实现。为了防止在对双向链表的插入与删除操作出现空指针异常,我们可以引入虚拟头节点和虚拟尾节点。

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class LRUCache {
    static class DLinkNode {
        public int key;
        public int val;
        public DLinkNode prev;
        public DLinkNode next;

        public DLinkNode() {
        }

        public DLinkNode(int key, int val) {
            this.key = key;
            this.val = val;
        }

        @Override
        public String toString() {
            return "{key=" + key + ", val=" + val + '}';
        }
    }

    public DLinkNode head; // 双向链表的头节点
    public DLinkNode tail; // 双向链表的尾节点
    public int usedSize; // 双向链表中有效的节点个数
    public Map<Integer, DLinkNode> cache;
    public int capacity;

    public LRUCache(int capacity) {
        this.head = new DLinkNode();
        this.tail = new DLinkNode();
        head.next = tail;
        tail.prev = head;
        cache = new HashMap<>();
        this.capacity = capacity;
    }

    public void put(int key, int val) {
        
    }

    public int get(int key) {
        
    }
}

对于 put() 方法,我们需要先判断 key 是否已经存储过。如果没有存储过,就把 key 和节点一起存放进 HashMap 里,节点也需要存储在链表的尾巴中,并且我们还需要判断链表中有效节点的个数,如果大于容量,则需要移除头部的节点。

java 复制代码
public void put(int key, int val) {
    // 一、先检查 key 是否已经存储过
    DLinkNode node = cache.get(key);
    // 二、没有存储过
    if (node == null) {
        DLinkNode dLinkNode = new DLinkNode(key, val);
        cache.put(key, dLinkNode);
        addToTail(dLinkNode);
        usedSize++;
        // 三、存储过
        if (usedSize > capacity) {
            DLinkNode remNode = removeHead();
            cache.remove(remNode.key);
            usedSize--;
        }
    } else {
        // 三、如果存储过
        node.val = val;
        moveToTail(node);
    }
}

/**
 * 将指定节点移动到链表尾部
 * @param node 需要移动的节点
 */
private void moveToTail(DLinkNode node) {
    removeNode(node); // 先移除
    addToTail(node); // 再将节点移动到尾部
}

private void removeNode(DLinkNode node) {
    node.prev.next = node.next;
    node.next.prev = node.prev;
}

private DLinkNode removeHead() {
    DLinkNode delNode = head.next;
    head.next = delNode.next;
    delNode.next.prev = head;
    return delNode;
}

/**
 * 将节点添加到双向链表的尾部
 * @param node 要添加的节点
 */
private void addToTail(DLinkNode node) {
    tail.prev.next = node;
    node.prev = tail.prev;
    tail.prev = node;
    node.next = tail;
}

关于 get() 方法,如果 key 不存在于缓存中,返回 -1。如果存在,直接返回对应的 val,并将该节点移动到链表尾部。

java 复制代码
public int get(int key) {
    DLinkNode node = cache.get(key);
    if (node == null) {
        return -1;
    }

    // 最近使用最多的节点移动到链表的尾巴
    moveToTail(node);
    return node.val;
}
相关推荐
重生之后端学习2 小时前
17. 电话号码的字母组合
java·开发语言·数据结构·算法·深度优先
0 0 02 小时前
CCF-CSP 32-2 因子化简(prime)【C++】考点:素数因子分解(试除法)
开发语言·数据结构·c++·算法
仰泳的熊猫2 小时前
题目1545:蓝桥杯算法提高VIP-现代诗如蚯蚓
数据结构·c++·算法·蓝桥杯
载数而行5202 小时前
复杂度问题
c语言·数据结构·c++·算法·排序算法
吕司2 小时前
LeetCode Hot Code——三数之和
数据结构·算法·leetcode
金枪不摆鳍2 小时前
hot100二分查找专题
数据结构·算法
代码改善世界2 小时前
【数据结构与算法】顺序表和链表题解
数据结构·链表
浅念-3 小时前
C++ STL list 容器
开发语言·数据结构·c++·经验分享·笔记·算法·list
重生之后端学习3 小时前
39. 组合总和
java·数据结构·算法·职场和发展·深度优先