Hot 146 LRU Cache 实现详解

一、问题背景

LRU(Least Recently Used)缓存是一种缓存淘汰策略:

  • 当缓存容量满时,删除最近最少使用的数据
  • 要求 get 和 put 操作的时间复杂度为 O(1)

二、数据结构选择

要实现 O(1) 操作,需要两种数据结构配合:

  1. HashMap:实现 O(1) 的查找
  • Map<Integer, Node>:key 到节点的映射

2.双向链表:实现 O(1) 的插入和删除,维护访问顺序

  • 头部:最近使用的节点
  • 尾部:最近最少使用的节点

三、核心设计:虚拟头尾节点

使用虚拟头尾节点简化边界处理:

head(虚拟)<-> Node1 <-> Node2 <-> Node3 <-> tail(虚拟)

↑最近使用 ↑最近最少使用

优势:

  • 统一处理:所有实际节点都有前驱和后继
  • 代码简洁:不需要特殊判断边界情况
  • 减少错误:避免空指针异常

四、完整代码实现

java 复制代码
import java.util.*;

class LRUCache {
    // 双向链表节点
    class Node {
        int key;
        int value;
        Node prev;
        Node next;
        Node() {}       
        Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    
    private int capacity;
    private Map<Integer, Node> cache;
    private Node head;  // 虚拟头节点
    private Node tail;  // 虚拟尾节点
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
        this.head = new Node();
        this.tail = new Node();
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        Node node = cache.get(key);
        if (node == null) {
            return -1;
        }
        moveToHead(node);  // 移到头部,标记为最近使用
        return node.value;
    }
    
    public void put(int key, int value) {
        Node node = cache.get(key);
        if (node == null) {
            // key不存在,插入新节点
            Node newNode = new Node(key, value);
            if (cache.size() >= capacity) {
                // 容量已满,删除最近最少使用的节点
                Node lastNode = removeTail();
                cache.remove(lastNode.key);
            }
            addToHead(newNode);
            cache.put(key, newNode);
        } else {
            // key已存在,更新值并移到头部
            node.value = value;
            moveToHead(node);
        }
    }
    
    // 将节点添加到头部
    private void addToHead(Node node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    
    // 移除节点
    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    // 将节点移到头部
    private void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }
    
    // 移除尾部节点(最近最少使用)
    private Node removeTail() {
        Node lastNode = tail.prev;
        removeNode(lastNode);
        return lastNode;
    }
}

五、关键方法详解

1. get(int key) - 获取值
java 复制代码
public int get(int key) {
    Node node = cache.get(key);
    if (node == null) {
        return -1;
    }
    moveToHead(node);  // 关键:移到头部标记为最近使用
    return node.value;
}

流程:

  1. 从 HashMap 查找:O(1)
  2. 不存在返回 -1
  3. 存在则移到头部(标记为最近使用)
  4. 返回 value
2. put(int key, int value) - 插入/更新
java 复制代码
public void put(int key, int value) {
    Node node = cache.get(key);
    if (node == null) {
        // 插入新节点
        Node newNode = new Node(key, value);
        if (cache.size() >= capacity) {
            Node lastNode = removeTail();
            cache.remove(lastNode.key);  // 从HashMap中删除
        }
        addToHead(newNode);
        cache.put(key, newNode);
    } else {
        // 更新已存在的节点
        node.value = value;
        moveToHead(node);
    }
}

两种情况:

  • key 不存在:创建新节点,容量满时删除尾部节点
  • key 已存在:更新 value 并移到头部
3. addToHead(Node node) - 添加到头部
java 复制代码
private void addToHead(Node node){
    node.prev=head;
    node.next=head.next;
    head.next.prev=node;
    head.next=node;
}

操作步骤:

原来:head <-> node1 <-> tail

插入 node:head <-> node <-> node1 <-> tail

  1. node.prev = head (node的前驱指向head)

  2. node.next = head.next (node的后继指向原来的第一个节点)

  3. head.next.prev = node (原来第一个节点的前驱指向node)

  4. head.next = node (head的后继指向node)

4. removeNode(Node node) - 移除节点
java 复制代码
private void removeNode(Node node){
    node.prev.next=node.next;
    node.next.prev=node.prev;
}

操作步骤:

java 复制代码
原来:prev <-> node <-> next
删除后:prev <-> next

1. node.prev.next = node.next  (前驱的后继指向node的后继)
2. node.next.prev = node.prev  (后继的前驱指向node的前驱)

六、执行流程示例

假设 capacity = 2,执行以下操作:

  1. put(1, 1)

cache: {1 -> Node(1,1)}

链表: head <-> Node(1,1) <-> tail

  1. put(2, 2)

cache: {1 -> Node(1,1), 2 -> Node(2,2)}

链表: head <-> Node(2,2) <-> Node(1,1) <-> tail

  1. get(1)

找到 Node(1,1),移到头部

链表: head <-> Node(1,1) <-> Node(2,2) <-> tail

返回: 1

  1. put(3, 3)

容量已满,删除尾部 Node(2,2)

插入新节点 Node(3,3) 到头部

cache: {1 -> Node(1,1), 3 -> Node(3,3)}

链表: head <-> Node(3,3) <-> Node(1,1) <-> tail

七、总结

LRU Cache 实现要点:

  1. 数据结构:HashMap + 双向链表
  2. 虚拟节点:简化边界处理
  3. 访问顺序:头部 = 最近使用,尾部 = 最近最少使用
  4. 时间复杂度:所有操作 O(1)
  5. 关键操作:get/put 时都要将节点移到头部

这是一个经典的数据结构组合应用,在面试中经常出现。掌握这个实现,对理解缓存机制和数据结构设计很有帮助。

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔1 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json