【算法--链表】146.LRU缓存--通俗讲解

算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解


通俗易懂讲解"LRU缓存"算法题目

一、题目是啥?一句话说清

设计一个LRU缓存,支持get和put操作,当缓存满时淘汰最久未使用的数据,且get和put操作的时间复杂度必须是O(1)。

示例:

  • 初始化:容量为2
  • put(1, 1) → 缓存: {1=1}
  • put(2, 2) → 缓存: {1=1, 2=2}
  • get(1) → 返回1,缓存: {2=2, 1=1}(1被访问,移到前面)
  • put(3, 3) → 缓存满,淘汰2,缓存: {1=1, 3=3}

二、解题核心

使用哈希表+双向链表。哈希表保证get操作O(1),双向链表维护使用顺序(最近使用的在头,最久未使用的在尾),保证put操作O(1)。

这就像有一个字典(哈希表)可以快速找到物品,还有一个排队队列(双向链表)记录物品的使用顺序,新用的或刚访问的放到队头,队尾的就是最久未用的,满了就淘汰队尾。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 哈希表的快速查找

  • 是什么:哈希表存储键到双向链表节点的映射,通过键可以立即找到对应的节点。
  • 为什么重要:这使得get操作可以在O(1)时间内完成,因为我们不需要遍历链表来查找节点。

2. 双向链表的顺序维护

  • 是什么:双向链表按使用顺序排列节点,最近使用的节点在头部,最久未使用的在尾部。
  • 为什么重要:当需要淘汰时,我们可以直接删除尾部节点;当访问或添加节点时,我们可以将其移到头部,这些操作都是O(1)。

3. 节点操作的原子性

  • 是什么:在get和put操作中,需要将节点移到链表头部,这涉及删除节点和添加节点的操作。
  • 为什么重要:这些操作必须正确更新节点的前后指针,否则会导致链表断裂或错误。同时,哈希表必须与链表同步更新。

四、看图理解流程(通俗理解版本)

假设缓存容量为2,操作序列如下:

  1. 初始化:缓存为空,双向链表为空,哈希表为空。

    • 链表:头 ←→ 尾(虚拟节点)
  2. put(1, 1)

    • 创建节点(1,1),添加到链表头部。
    • 哈希表记录键1指向该节点。
    • 链表:头 ←→ 1 ←→ 尾
  3. put(2, 2)

    • 创建节点(2,2),添加到链表头部。
    • 哈希表记录键2指向该节点。
    • 链表:头 ←→ 2 ←→ 1 ←→ 尾
  4. get(1)

    • 通过哈希表找到节点1
    • 将节点1从链表中删除(断开连接),然后添加到头部。
    • 链表:头 ←→ 1 ←→ 2 ←→ 尾
    • 返回1。
  5. put(3, 3)

    • 缓存已满(容量2),需要淘汰最久未使用的节点2(在尾部)。
    • 删除节点2,并从哈希表中移除键2。
    • 创建节点(3,3),添加到链表头部。
    • 哈希表记录键3指向该节点。
    • 链表:头 ←→ 3 ←→ 1 ←→ 尾

五、C++ 代码实现(附详细注释)

cpp 复制代码
#include <iostream>
#include <unordered_map>
using namespace std;

// 双向链表节点定义
struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    unordered_map<int, DLinkedNode*> cache; // 哈希表:键 -> 节点指针
    DLinkedNode* head; // 虚拟头节点
    DLinkedNode* tail; // 虚拟尾节点
    int size; // 当前缓存大小
    int capacity; // 缓存容量

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

    // 删除节点
    void removeNode(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    // 移动节点到头部
    void moveToHead(DLinkedNode* node) {
        removeNode(node);
        addToHead(node);
    }

    // 删除尾部节点并返回
    DLinkedNode* removeTail() {
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }

public:
    LRUCache(int _capacity) : capacity(_capacity), size(0) {
        // 创建虚拟头尾节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }

    int get(int key) {
        if (!cache.count(key)) {
            return -1;
        }
        // 通过哈希表定位节点
        DLinkedNode* node = cache[key];
        // 移动节点到头部表示最近使用
        moveToHead(node);
        return node->value;
    }

    void put(int key, int value) {
        if (cache.count(key)) {
            // 键已存在,更新值并移到头部
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        } else {
            // 创建新节点
            DLinkedNode* node = new DLinkedNode(key, value);
            // 添加到哈希表和链表头部
            cache[key] = node;
            addToHead(node);
            size++;
            // 如果超过容量,删除尾部节点
            if (size > capacity) {
                DLinkedNode* removed = removeTail();
                cache.erase(removed->key);
                delete removed;
                size--;
            }
        }
    }
};

// 测试代码
int main() {
    LRUCache lru(2);
    lru.put(1, 1);
    lru.put(2, 2);
    cout << lru.get(1) << endl; // 返回 1
    lru.put(3, 3); // 淘汰键 2
    cout << lru.get(2) << endl; // 返回 -1 (未找到)
    lru.put(4, 4); // 淘汰键 1
    cout << lru.get(1) << endl; // 返回 -1 (未找到)
    cout << lru.get(3) << endl; // 返回 3
    cout << lru.get(4) << endl; // 返回 4
    return 0;
}

六、时间空间复杂度

  • 时间复杂度:get和put操作都是O(1)。因为哈希表操作平均O(1),双向链表的添加、删除和移动操作也是O(1)。
  • 空间复杂度:O(capacity),哈希表和双向链表存储的节点数最多为capacity。

七、注意事项

  • 虚拟头尾节点:使用虚拟头尾节点可以简化链表操作,避免处理空指针或边界情况。
  • 内存管理:在C++中,需要手动删除被淘汰的节点,防止内存泄漏。
  • 线程安全:此实现不是线程安全的,如果在多线程环境中使用,需要添加锁机制。
  • 哈希表更新:在删除节点时,必须同时从哈希表中移除对应的键,否则会导致哈希表中有悬垂指针。
  • 节点移动顺序:在moveToHead操作中,先removeNode再addToHead,确保节点正确移动到头部。

算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解

相关推荐
倒头就睡的小比特19 小时前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!19 小时前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
晨米酱20 小时前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
猎头南楼20 小时前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
旖旎夜光21 小时前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark21 小时前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her11 天前
并查集-听课笔记
笔记·算法·并查集
码流子1 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
彧azz1 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
another heaven1 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法