LRU 缓存的设计与实现

目录

[一、LRU 缓存的核心诉求](#一、LRU 缓存的核心诉求)

二、数据结构选型与设计思路

[1. 双向链表:维护访问顺序的 "时间轴"](#1. 双向链表:维护访问顺序的 “时间轴”)

[2. 哈希表:实现 key 的 O (1) 寻址](#2. 哈希表:实现 key 的 O (1) 寻址)

[3. 组合设计:"哈希表 + 双向链表" 的协同工作](#3. 组合设计:“哈希表 + 双向链表” 的协同工作)

三、代码实现

[1. 类结构定义](#1. 类结构定义)

[2. get 方法实现:查询并更新访问顺序](#2. get 方法实现:查询并更新访问顺序)

[3. put 方法实现:插入、更新与容量控制](#3. put 方法实现:插入、更新与容量控制)

四、复杂度与边界场景分析

[1. 时间复杂度](#1. 时间复杂度)

[2. 边界场景处理](#2. 边界场景处理)

五、测试验证与工程价值

六、总结


在高并发与大数据场景中,缓存是提升系统性能的关键手段,而 LRU(Least Recently Used,最近最少使用) 作为最经典的缓存淘汰策略之一,其设计与实现蕴含着深刻的工程智慧。本文将从底层原理出发,详细拆解 LRU 缓存的设计思路,并提供可直接落地的 C++ 实现。

一、LRU 缓存的核心诉求

LRU 策略的核心逻辑是:当缓存容量不足时,优先淘汰 "最久未被访问" 的缓存项。要实现这一策略,我们需要解决两个关键问题:

  1. 快速查询 :给定 key,需在 O (1) 时间内判断是否存在并获取 value。
  2. 快速维护访问顺序 :每次访问(get 或 put)后,需将缓存项标记为 "最近使用";当容量不足时,需快速找到并淘汰 "最久未使用" 的项。

若仅用哈希表 ,虽能满足 "快速查询",但无法高效维护访问顺序;若仅用链表 ,查询操作会退化为 O (n),性能无法接受。因此,我们需要一种组合数据结构来突破这一困境。

二、数据结构选型与设计思路

1. 双向链表:维护访问顺序的 "时间轴"

使用双向链表存储缓存项,节点按 "最近使用" 到 "最久未使用" 的顺序排列:

  • 链表头部:最近被访问的缓存项。
  • 链表尾部:最久未被访问的缓存项(淘汰时的目标)。

双向链表的优势在于:

  • 节点移动(标记为 "最近使用")的时间复杂度为 O (1)(只需调整指针)。
  • 淘汰操作(删除尾部节点)的时间复杂度为 O (1)。

2. 哈希表:实现 key 的 O (1) 寻址

使用 ** 哈希表(unordered_map)** 存储 key 到 "链表节点迭代器" 的映射,这样可以:

  • 快速判断 key 是否存在(O (1) 时间)。
  • 直接定位到对应的链表节点(O (1) 时间),从而避免遍历链表的开销。

3. 组合设计:"哈希表 + 双向链表" 的协同工作

哈希表的 value 是链表节点的迭代器,这样可以在 O (1) 时间内完成:

  • 查询(get) :通过哈希表找到节点,返回 value 并将节点移到链表头部。
  • 插入 / 更新(put) :若 key 存在则更新值并移到头部;若不存在则插入新节点到头部,若容量不足则删除尾部节点。

三、代码实现

1. 类结构定义

cpp 复制代码
class LRUCache {
public:
    // 构造函数:初始化缓存容量
    LRUCache(int capacity) : _capacity(capacity) {}
    
    // 查询操作:O(1) 时间复杂度
    int get(int key);
    
    // 插入/更新操作:O(1) 时间复杂度
    void put(int key, int value);

private:
    // 链表节点迭代器类型(指向存储 key-value 的双向链表节点)
    using ListIter = std::list<std::pair<int, int>>::iterator;
    
    std::unordered_map<int, ListIter> _hashMap;  // key -> 链表节点迭代器
    std::list<std::pair<int, int>> _lruList;     // 双向链表,维护访问顺序
    int _capacity;                               // 缓存最大容量
};

2. get 方法实现:查询并更新访问顺序

cpp 复制代码
int LRUCache::get(int key) {
    auto it = _hashMap.find(key);
    if (it == _hashMap.end()) {
        return -1;  // key 不存在
    }
    
    // 将节点移到链表头部(标记为最近使用)
    _lruList.splice(_lruList.begin(), _lruList, it->second);
    return it->second->second;  // 返回 value
}
  • splice 是双向链表的核心操作,可在 O (1) 时间内将节点移动到任意位置(这里移到头部)。

3. put 方法实现:插入、更新与容量控制

cpp 复制代码
void LRUCache::put(int key, int value) {
    auto it = _hashMap.find(key);
    if (it != _hashMap.end()) {
        // 情况1:key 已存在,更新 value 并标记为最近使用
        ListIter node = it->second;
        node->second = value;  // 更新 value
        _lruList.splice(_lruList.begin(), _lruList, node);
    } else {
        // 情况2:key 不存在,插入新节点
        // 若容量已满,淘汰最久未使用的节点(链表尾部)
        if (_lruList.size() == _capacity) {
            auto tail = _lruList.back();
            _hashMap.erase(tail.first);  // 从哈希表中删除
            _lruList.pop_back();         // 从链表中删除
        }
        // 插入新节点到链表头部
        _lruList.push_front({key, value});
        _hashMap[key] = _lruList.begin();  // 哈希表记录映射
    }
}
  • 插入新节点前,若容量已满,需先删除链表尾部的 "最久未使用" 节点,并同步从哈希表中移除对应的 key。

四、复杂度与边界场景分析

1. 时间复杂度

  • get 操作:O (1)(哈希表查询 + 链表节点移动)。
  • put 操作:O (1)(哈希表插入 / 删除 + 链表节点移动 / 删除)。

这一复杂度满足了题目中 "O (1) 平均时间复杂度" 的要求。

2. 边界场景处理

  • 空缓存 / 空 key :get 操作返回 -1。
  • 容量为 1:每次插入都会淘汰之前的节点,保证缓存中始终只有最新的一个项。
  • 重复 put 同一 key :会更新其 value 并将其标记为 "最近使用"。

五、测试验证与工程价值

我们通过一个典型场景验证实现的正确性:

cpp 复制代码
int main() {
    LRUCache cache(2);  // 容量为 2 的 LRU 缓存
    cache.put(1, 1);
    cache.put(2, 2);
    cout << cache.get(1) << endl;  // 输出 1(1 被标记为最近使用)
    cache.put(3, 3);    // 容量已满,淘汰最久未使用的 2
    cout << cache.get(2) << endl;  // 输出 -1(2 已被淘汰)
    cache.put(4, 4);    // 容量已满,淘汰最久未使用的 1
    cout << cache.get(1) << endl;  // 输出 -1(1 已被淘汰)
    cout << cache.get(3) << endl;  // 输出 3(3 被标记为最近使用)
    cout << cache.get(4) << endl;  // 输出 4(4 被标记为最近使用)
    return 0;
}

输出结果与预期完全一致,证明了实现的正确性。

六、总结

LRU 缓存的设计是 "数据结构协同作战" 的经典案例:双向链表 解决了 "访问顺序维护" 的问题,哈希表解决了 "快速寻址" 的问题,二者结合让 LRU 策略在 O (1) 时间复杂度下高效运行。

这一设计思路不仅适用于算法题,更在工业级缓存系统(如 Redis 的缓存策略)中被广泛应用。理解其底层逻辑,有助于我们在复杂业务场景中设计更高效的缓存方案。

相关推荐
Wang's Blog3 小时前
Java 项目实战: 交友项目:登录链路回顾与今日任务地图
java·开发语言·状态模式·交友
砚底藏山河3 小时前
python量化:行情数据回溯
java·开发语言·python·金融·maven
今年下半年3 小时前
AI智能体项目实战记录(02):初始化 Python 项目
开发语言·人工智能·python·fastapi
Williamtym4 小时前
PickGo Lab实践:用浏览器、Python和MCP串起机器人搬运仿真
开发语言·python·机器人
DongQiShanRen4 小时前
六根硬件映射(四):归属澄清、两处不一致与卷十一总表
开发语言·人工智能·算法·rust·哈希算法·集成学习
CS创新实验室4 小时前
Python 对象“死“了之后去了哪?——引用计数与分代垃圾回收
开发语言·jvm·python
quantdash_cc4 小时前
Pandas 处理分钟级股票行情:从原始数据清洗到 K 线聚合
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
yu_zhidun4 小时前
企业电脑监控软件怎么选:别只看功能清单,先看这 6 个底层能力
开发语言·电脑·php·域智盾系统
把我格子衫拿来5 小时前
VTK 9.5 与 Qt 6 联合开发:从嵌入三维窗口到可维护的桌面架构
c++·python·qt·vtk·三维可视化
无忧.芙桃5 小时前
C++语言原理与实践(十六):多态机制讲解
开发语言·c++·多态