当前位置:首页 > 编程笔记 > 正文
已解决

【LRU缓存机制】+ 双向链表一些基础操作

来自网友在路上 177877提问 提问时间:2023-09-25 14:25:14阅读次数: 77

最佳答案 问答题库778位专家为你答疑解惑

文章目录

  • Tag
  • 题目来源
  • 题目解读
  • 解题方法
    • 方法一:哈希表+双向链表
  • 知识回顾
    • 双向链表的几个基本操作
  • 写在最后

Tag

【哈希表】【双向链表】【设计数据结构】【2023-09-24】


题目来源

146. LRU 缓存

146题目.gif


题目解读

LRU 是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

本题需要设计实现 LRUCache 类,具体地需要实现:

  • LRUCache(int capacity):以正整数作为容量 capacity 初始化 LRU 缓存;
  • int get(int key):如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value):如果关键字 key 已经存在,则变更其数据值 value;如果不存在,则向缓存中插入该组 key-value。如果插入操作导致关键字数量超过 capacity,则应该 逐出 最久未使用的关键字。

要求函数 get()put() 必须以 O ( 1 ) O(1) O(1) 的平均复杂度运行。


解题方法

今天又是认真学习研究 LRU缓存机制 官方题解的一天!

方法一:哈希表+双向链表

使用什么样的数据结构?

对于设计这种题目,要明确每个步骤的时间复杂度要求,如果数据给定的操作是常数级别的,那么这个操作可用 O ( n ) O(n) O(n) 的算法;否则就要往 O ( 1 ) O(1) O(1) 或者 O ( l o g n ) O(logn) O(logn) 去考虑;

  • 对于创建操作 LRUCacheCreate,只有一次操作,一般就是 O ( n ) O(n) O(n) 了;
  • 对于获取操作 LRUCacheGet,如果要求 O ( 1 ) O(1) O(1),一般就是数组和哈希表了(大概率就是哈希表了);
  • 对于插入操作 LRUCachePut,如果要求 O ( 1 ) O(1) O(1),数组放入最后一个位置和链表放入第一个元素的操作都是 O ( 1 ) O(1) O(1)

如果插入的关键字数量超过 capacity,那么就应该逐出最久未使用的关键字。这表明插入和删除操作要在头部和尾部进行,能够在头部和尾部进行插入和删除操作的是队列,但是双向链表最佳。

具体实现

最终使用的数据结构是双向链表和哈希表。具体地:

  • 哈希表的键为 key,对应的值为 key 在双向链表中的位置;
  • 双向链表按照被使用的顺序存储了这些键值对,靠近双向链表头部的键值对表示最近使用的,靠近双向链表尾部的键值对表示最久未使用的。

这样,我们可以先通过哈希表来确定某一个 key 在缓存中的位置,访问了这个 key 之后,这个 key 就成为了最近访问的,就需要 移动到双向链表的头部,对应的操作就是 get 操作。具体如下:

  • 如果 key 不存在,则返回 -1
  • 如果 key 存在,则 key 对应的链表节点就是最近被使用的节点。需要将其在双向链表中的位置移动到头部,最后要返回该节点的值。

对于 put 操作,首先需要判断 key 是否存在:

  • 如果 key 不存在,需要使用 keyvalue 创建一个新的节点,将新建的节点加入到双向链表的头部(表示最近使用的),并将 key 和该节点加入到哈希表中。加入了一个新的双向链表节点之后需要判断是否超出了缓存的容量,如果超出了需要将双向链表的尾部节点删除(表示删除最近未使用的),并删除哈希表中的对应项;
  • 如果 key 存在,需要先通过哈希表定位,再将对应的节点值更新为 value,并将该节点移动到双向链表的头部。

上述各项操作中,访问哈希表的时间复杂度为 O ( 1 ) O(1) O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O ( 1 ) O(1) O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O ( 1 ) O(1) O(1) 时间内完成。

实现代码

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, *tail;int size;int capacity;
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;}// key 存在,定位,移到头部DLinkedNode* node = cache[key];moveToHead(node);return node->value;}void put(int key, int value) {// key 不存在,创建,加入哈希表,加入到头部,判断是否超容if (!cache.count(key)) {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;}}else { // key 存在,定位,修改,移到头部DLinkedNode* node = cache[key];node->value = value;moveToHead(node);}}// 将节点 node 移动到双向链表头部void moveToHead(DLinkedNode* node) {removeNode(node);addToHead(node);}// 将节点 node 加入到双向链表头部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;}// 移除节点 nodevoid removeNode(DLinkedNode* node) {node->prev->next = node->next;node->next->prev = node->prev;}
};/*** Your LRUCache object will be instantiated and called as such:* LRUCache* obj = new LRUCache(capacity);* int param_1 = obj->get(key);* obj->put(key,value);*/

复杂度分析

时间复杂度:对于 putget 都是 O ( 1 ) O(1) O(1)

空间复杂度: O ( c a p a c i t y ) O(capacity) O(capacity),因为哈希表和双向链表最多存储 c a p a c i t y + 1 capacity+1 capacity+1 个元素。


知识回顾

双向链表的几个基本操作

接下来以图示的方式,来介绍一下上述成员方法实现中的一些双链表操作,包括:

  • 将节点 node 增加到双向链表头部;
  • 在双向链表中移除某个节点 node
  • 其他的一些操作(移除尾结点,移动节点到头部)都可以通过以上两种操作实现。

初始化

在双向链表的实现中,使用一个伪头部和伪尾部来标记界限,这样在增加节点和删除节点的时候就不要检查相邻两个节点是否存在了。

struct DLinkedNode {int value;DLinkedNode* prev;DLinkedNode* next;DLinkedNode(): value(0), prev(nullptr), next(nullptr) {}DLinkedNode(int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class UseDLinkedNode {
public:UseDLinkedNode() {// 使用伪头部和伪尾部节点head = new DLinkedNode();tail = new DLinkedNode();head->next = tail;tail->prev = head;}void addToHead(DLinkedNode* node);      // node 头插void removeNode(DLinkedNode* node);     // 删除 nodevoid moveToHead(DLinkedNode* node);DLinkedNode* removeTail();
};

将节点 node 增加到双向链表头部

该操作就是将 node 插入到 dummy headdummy tail 之间:

(1)首先将 nodeprevnext 指针更新好,即 node->prev = headnode->next = head->next

(2)设置伪头部下一个节点的 prev(现在伪头节点下一个节点为伪尾部)节点,首先定位到伪头部下一个节点即 head->next

(3)伪头部下一个节点的 prevnode

(4)连接伪头部的下一个节点;

(5)最后,将节点 node 加入到双向链表头部即 node 的头插操作完成。

在双向链表中移除节点 node

在双向链表中移除某个节点,只需要修改指针的指向,使得双链表跳过该节点。

void UseDLinkedNode::removeNode(DLinkedNode* node) { // 删除 nodenode->prev->next = node->next;node->next->prev = node->prev;
}    

(1)修改 node->prev 的下一个节点的指向即 node->prev->next = node->next

(2)修改 node->next 的前一个节点的指向即 node->next->prev = node->prev

(3)最后删除 node 后的结果如下图所示。

移除尾结点

DLinkedNode* UseDLinkedNode::removeTail() {DLinkedNode* node = tail->prev; // 先找到removeNode(node);               // 再移除return node;                    // 最后返回被移除的尾节点
}

移动节点到头部

void UseDLinkedNode::moveToHead(DLinkedNode* node) {removeNode(node);   // 先移除addToHead(node);    // 加到头部
}

写在最后

如果文章内容有任何错误或者您对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家有更优的时间、空间复杂度方法,欢迎评论区交流。

最后,感谢您的阅读,如果感到有所收获的话可以给博主点一个 👍 哦。

查看全文

99%的人还看了

猜你感兴趣

版权申明

本文"【LRU缓存机制】+ 双向链表一些基础操作":http://eshow365.cn/6-13492-0.html 内容来自互联网,请自行判断内容的正确性。如有侵权请联系我们,立即删除!