面试题 16.25. LRU 缓存

题目链接: 面试题 16.25. LRU 缓存
📙题目描述:

✏️题目分析:

我们通过阅读题目可知题目中出现键值对,我们遇到键值对就要想到哈希表。题目中让我们构建一个"最近最少使用",并且支持插入和删除数据,对于有出入顺序的问题,我们就要想到栈,队列或者是链表。这道题我们可以用双向链表。

通过示例我们可以发现这道题在访问数据的时候是随机的,由于哈希表支持能够在时间复杂度为O(1)内查找元素,如果哈希表中的值包含链表的结点信息,就能实现在O(1)内定位到链表中的某个结点。所以我们将来可以定义哈希表的结构为:

cpp 复制代码
unordered_map<键值,链表的结点>

这样就能实现通过键值来定位到链表中的结点。

由于哈希表只负责定位查找功能,所以我们的双向链表结点中就应该包含keyvalue,所以双向链表的结点我们可以定义成:

cpp 复制代码
struct DLinkedNode
{
	int key,value;
	DLinkedNode* prev;//双向链表中的上一个结点
	DLinkedNode* next;//双向链表中的下一个结点
};

🌵对于get操作:

  • 如果密钥key不存在
    返回-1
  • 如果密钥key存在,则获取密钥的值。并且当前密钥对应的结点为最近最新使用的结点。我们把最近最少使用的结点放在链表的尾部,最近最新使用的结点放在链表的头部。

🌿如何把最近最新使用的结点放在双向链表的头部呢?

我们可以先将此结点移出双向链表,再将此结点添加到链表的头部,所以这里就需要实现两种方法removeNodeaddToHead

removeNode

cpp 复制代码
void removeNode(DLinkedNode* node)
{
	node->prev->next->node->next;//node结点的上一个结点的next指向node结点的下一个结点
	node->next->prev->node->prev;//node结点的下一个结点的prev指向node结点的上一个结点
}


addToHead

cpp 复制代码
void addToHead(DLinkedNode* node)
{
	node->prev = head;
	node->next = head->next;
	head->next->prev = node;
	head->next = node;
}

🌵对于put操作:

  • 如果密钥key不存在

    keyvalue创建一个新的结点,并用key将此结点添加到哈希表中,调用addToHead将此结点放到双向链表的头部,判断结点的数量是否超出了容量,如果超出容量,删除掉最近最少使用的那个结点,即尾部结点,并删除哈希表中对应的项。

  • 如果密钥key存在

    get操作类似,先通过key使用哈希表找到此结点在双向链表中的位置,更新value,用moveToHead将此结点移动到链表的头部

    如果链表的数量大于链表的容量,我们就需要删除最近最少使用的结点。双向链表的尾部结点即为最近最少使用的,我们要删除这个结点就要先找到这个结点。我们使用removeTail()找到这个结点并删除,并且删除哈希表中对应的项,delete掉这个结点防止内存泄漏。

moveToHeaad

cpp 复制代码
void moveToHead(DLinkedNode* node)
{
	removeNode(node);
	addToHead(node);
}

removeTail

cpp 复制代码
DLinkedNode* removeTail()
{
	DLinkedNode* node = tail->prev;
	removeNode(node);
	
	return node;
}

🔖细节问题:我们可以定义一个伪头节点和一个伪尾结点,这样我们在增加或者删除结点的时候就不需要判断相邻的结点是否存在。

🐾 代码实现:

cpp 复制代码
//定义双向链表结点
struct DLinkedNode
{
    int key, value;
    DLinkedNode* prev; //双向链表结点的上一个结点
    DLinkedNode* next;//双向链表结点的下一个结点

    DLinkedNode(int _key = 0, int _value = 0)
    :key(_key)
    ,value(_value)
    ,prev(nullptr)
    ,next(nullptr)
{
}

};
class LRUCache
{
public:
    //构造函数
    LRUCache(int _capacity)
        :capacity(_capacity)
        , size(0)
    {
        //使用伪头结点和伪尾结点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }

    int get(int key)
    {
        //首先判断key是否存在
        if (!cache.count(key))
        {
            //说明key不存在
            return -1;
        }
        else
        {
            //key存在
            DLinkedNode* node = cache[key];//通过哈希表定位到key所对应的结点
            //将key对应的结点移动到双向链表的头部
            moveToHead(node);

            return node->value;//最后返回该结点的值
        }
    }

    void put(int key, int value)
    {
        //首先判断key是否存在
        if (!cache.count(key))
        {
            //key不存在
            DLinkedNode* node = new DLinkedNode(key, value);//使用key和value创建一个新的结点
            addToHead(node);//将新结点添加到双向链表的头部
            cache[key] = node;//将新结点添加到哈希表中
            ++size;
            if (size > capacity)
            {
                //如果节点数超出双向链表的容量
                DLinkedNode* removed = removeTail();//删除尾部的结点
                cache.erase(removed->key);//删除哈希表中对应的项
                delete removed;//防止内存泄漏
                --size;
            }
        }
        else
        {
            //key存在
            DLinkedNode* node = cache[key];//通过哈希表定位key对应的node结点
            node->value = value;//修改node对应的value值
            moveToHead(node);//将该节点移动到双向链表的头部
        }
    }

    void moveToHead(DLinkedNode* node)
    {
        removeNode(node);
        addToHead(node);
    }

    void removeNode(DLinkedNode* node)
    {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void addToHead(DLinkedNode* node)
    {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }

    DLinkedNode* removeTail()
    {
        DLinkedNode* node = tail->prev;
        removeNode(node);

        return node;
    }
private:
    unordered_map<int, DLinkedNode*> cache;//通过哈希表实现O(1)的链表结点查找
    DLinkedNode* head;//伪头节点
    DLinkedNode* tail;//伪尾结点
    //有伪节点的存在,就不需要查找相邻结点是否存在
    int size;  //有效元素个数 
    int capacity;//空间大小
};

写完发现的问题:如果新添加的结点为0,0,和伪结点一样呢?

不影响,因为哈希表中并没有把伪结点的键值key添加到哈希表中。

相关推荐
亮子AI2 小时前
【Nginx】怎样清除 Nginx 的缓存?
运维·nginx·缓存
小白程序员成长日记3 小时前
2025.11.09 力扣每日一题
算法·leetcode·职场和发展
7澄13 小时前
深入解析 LeetCode 1572:矩阵对角线元素的和 —— 从问题本质到高效实现
java·算法·leetcode·矩阵·intellij-idea
诗9趁年华3 小时前
缓存三大问题深度解析:穿透、击穿与雪崩
java·spring·缓存
whltaoin3 小时前
【JAVA全栈项目】弧图图-智能图床SpringBoot+MySQL API接口结合Redis+Caffeine多级缓存实践解析
java·redis·spring·缓存·caffeine·多级缓存
程序员东岸3 小时前
数据结构精讲:从栈的定义到链式实现,再到LeetCode实战
c语言·数据结构·leetcode
升鲜宝供应链及收银系统源代码服务15 小时前
升鲜宝生鲜配送供应链管理系统---PMS--商品品牌多语言存储与 Redis 缓存同步实现
java·开发语言·数据库·redis·缓存·开源·供应链系统
sin_hielo15 小时前
leetcode 1611
算法·leetcode
来荔枝一大筐16 小时前
C++ LeetCode 力扣刷题 541. 反转字符串 II
c++·算法·leetcode