算法题 LRU 缓存

146. LRU 缓存

问题描述

设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。要求实现以下功能:

  • LRUCache(int capacity) 初始化缓存容量
  • int get(int key) 获取键对应的值(不存在返回 -1)
  • void put(int key, int value) 插入或更新键值对(容量满时淘汰最久未使用的键)

示例

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存 {1=1}
lRUCache.put(2, 2); // 缓存 {1=1, 2=2}
lRUCache.get(1);    // 返回 1(更新使用顺序)
lRUCache.put(3, 3); // 淘汰 key 2,缓存 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1(已淘汰)
lRUCache.put(4, 4); // 淘汰 key 1,缓存 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

算法设计

使用 哈希表 + 双向链表 实现:

  1. 双向链表:维护访问顺序,头部是最新访问节点,尾部是最久未用节点
  2. 哈希表:存储键到链表节点的映射,实现 O(1) 访问

核心操作

  • get 操作
    1. 从哈希表获取节点
    2. 若存在则移动节点到链表头部
  • put 操作
    1. 若键存在:更新值并移动节点到头部
    2. 若键不存在:
      • 创建新节点并添加到头部
      • 若容量超限:删除尾部节点并移除哈希表对应项

代码实现

import java.util.HashMap;
import java.util.Map;

class LRUCache {
    // 双向链表节点类
    class DLinkedNode {
        int key;    // 存储key用于删除尾部节点时移除哈希表对应项
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    // 核心数据结构
    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private int capacity;   // 缓存容量
    private int size;       // 当前缓存大小
    private DLinkedNode head, tail; // 虚拟头尾节点(哨兵节点)

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 初始化双向链表(带哨兵节点)
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1; // 键不存在
        }
        moveToHead(node); // 更新为最近使用
        return node.value;
    }
    
    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 创建新节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            cache.put(key, newNode); // 加入哈希表
            addToHead(newNode);      // 添加到链表头部
            size++;
            if (size > capacity) {
                // 容量超限:删除尾部节点
                DLinkedNode tail = removeTail();
                cache.remove(tail.key); // 从哈希表移除
                size--;
            }
        } else {
            // 更新已有节点
            node.value = value;
            moveToHead(node); // 更新为最近使用
        }
    }

    // 添加节点到链表头部
    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    // 从链表中移除节点
    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    // 移动节点到头部(先移除再添加)
    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    // 移除尾部节点并返回(最久未使用)
    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev; // 实际尾节点(非哨兵)
        removeNode(res);
        return res;
    }
}

算法过程

1. 添加节点到头部

  head     node     nextNode
  [ ] --> [node] --> [ ]
  [ ] <-- [node] <-- [ ]

步骤:

  1. node.prev = head
  2. node.next = head.next
  3. head.next.prev = node
  4. head.next = node

2. 移除节点

  prevNode     node     nextNode
  [ ] --> [node] --> [ ]
  [ ] <-- [node] <-- [ ]

步骤:

  1. node.prev.next = node.next
  2. node.next.prev = node.prev

复杂度分析

  • 时间复杂度:O(1)
    • getput 操作均通过哈希表定位(O(1))
    • 链表操作(添加/删除/移动)均为 O(1)
  • 空间复杂度:O(capacity)
    • 哈希表和链表存储容量级别的元素

测试用例

public static void main(String[] args) {
    LRUCache cache = new LRUCache(2);
    
    cache.put(1, 1);
    cache.put(2, 2);
    System.out.println(cache.get(1)); // 1(更新使用顺序)
    
    cache.put(3, 3);                 // 淘汰 key 2
    System.out.println(cache.get(2)); // -1(已淘汰)
    
    cache.put(4, 4);                 // 淘汰 key 1
    System.out.println(cache.get(1)); // -1
    System.out.println(cache.get(3)); // 3
    System.out.println(cache.get(4)); // 4
    
    // 特殊测试:容量为1
    LRUCache cache2 = new LRUCache(1);
    cache2.put(1, 1);
    cache2.put(2, 2);                // 淘汰 key 1
    System.out.println(cache2.get(1)); // -1
    System.out.println(cache2.get(2)); // 2
}

关键点

  1. 双向链表作用

    • 维护访问顺序(头新尾旧)
    • 支持 O(1) 时间完成节点移动/删除
  2. 哈希表作用

    • 提供 O(1) 的键值访问
    • 存储键到链表节点的映射
  3. 哨兵节点优势

    • 简化链表操作(无需判空)
    • 统一处理头尾操作
  4. 节点存储 key 的原因

    • 删除尾部节点时,需通过节点中的 key 删除哈希表对应项
  5. 操作顺序

    • 添加新节点时:先更新哈希表,再更新链表
    • 淘汰节点时:先删除链表节点,再删除哈希表项

常见问题

  1. 为什么需要双向链表?单链表不行吗?

    • 双向链表支持 O(1) 时间删除任意节点(单链表删除节点需遍历找前驱)
  2. 哈希表为什么存节点而不直接存值?

    • 需要通过节点操作链表(移动/删除),仅存值无法定位链表位置
  3. 如何保证 O(1) 时间复杂度?

    • 哈希表保证 O(1) 查找
    • 双向链表保证 O(1) 插入/删除
  4. 哨兵节点如何简化操作?

    • 确保头尾操作统一(无需特殊处理空链表或单节点情况)
  5. 为什么节点要存 key?

    • 删除尾部节点时,需要同时删除哈希表中对应的键(通过节点中的 key)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值