0
点赞
收藏
分享

微信扫一扫

关于LRU缓存简单记录以及代码补全。


目录

  • ​​大概思路​​
  • ​​时间空间复杂度分析​​
  • ​​指针操作具体细节​​
  • ​​代码​​
  • ​​双向链表设计​​
  • ​​私有成员变量设计:​​
  • ​​构造函数和析构函数设计:​​
  • ​​get与put具体设计​​
  • ​​双向指针的具体细节​​
  • ​​添加到头节点函数​​
  • ​​删除尾节点函数​​
  • ​​删除节点函数​​
  • ​​删除节点函数​​
  • ​​感想​​


今天面试考到LRU,太紧张了,完全傻了。。。赶紧做个记录

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

LRU 缓存:
设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

示例:

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

大概思路

这一题面试官说用到双向链表和哈希map。
双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
哈希表,通过缓存数据的键映射到其在双向链表中的位置。
注意双向链表中存储的元素是key, value;
哈希表中存储的是key和双向链表节点。

太紧张了,导致一开始都不知道map的value具体含义,还以为是频率计数用的。。。其实并无特殊含义,仅仅是一个值。
get操作:
1、判断key是否存在,不存在返回-1
2、如果存在,返回该节点值并且将对应的节点移动到双向链表的头部。
put操作:
1、判断key是否存在
2、key不存在,使用key和value创建一个新的节点,然后在双向链表头部插入这个键值对。
3、将 key 和该节点添加进哈希表中。
4、判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
5、key存在。先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

时间空间复杂度分析

访问哈希表的时间复杂度为 O(1)
在双向链表头和尾部增减节点时间复杂度也是O(1)

指针操作具体细节

将一个节点移动到头部可以分为:
1、删除该节点
2、在头节点前插入一个相同的节点

还有就是虚拟头节点和虚拟尾节点是使用:
添加节点和删除节点的时候就就比较方便了,这个在单向链表的设计中也有涉及:
​​​707. 设计链表​​ 下面给出具体的代码:

代码

双向链表设计

关于双向链表节点的定义

class 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) {}
};

关于LRUCache类的设计:

私有成员变量设计:

private:
//哈希表
unordered_map<int, DLinkedNode*> cache;
//虚拟头指针和虚拟尾指针
DLinkedNode* head;
DLinkedNode* tail;
//当前LRUCache中元素(键值对)个数
int size;
//LRUCache最多能容纳多少键值对
int capacity;

构造函数和析构函数设计:

构造函数:
这里给出最大容量,我们还需要初始化size的大小和主动添加虚拟头指针和虚拟尾指针,并将两者链接起来

LRUCache(int capacity) {
_capacity = capacity;
_size = 0;
dummyhead = new DLinkedNode();
dummytail = new DLinkedNode();
dummyhead->next = dummytail;
dummytail->prev = dummyhead;
}

析构函数:
主要是将两个虚拟头节点和虚拟尾节点释放掉

~LRUCache() {
delete head;
delete tail;
}

get与put具体设计

get函数:

int get(int key)
{
//如果找不到,返回-1
if(chache.find(key) == chache.end())
{
return -1;
}
//如果key存在
//哈希定位
DLinkedNode* node = chache[key];
//将该节点添加到双向链表头部
moveHead(node);
//返回该节点值
return node->value;
}

put函数:

void put(int key, int value)
{
//如果key不存在,创建一个新节点
if(cache.find(key) == cache.end())
{
//如果预计超出容量,删除双向链表尾部节点
if(size + 1 > capacity)
{
//删除双向链表尾部节点
DLinkedNode* deleted_node = deleteTail();
//删除哈希表中对应部分
cache.erase(deleted_node->key);
delete deleted_node;
size--;
}
DLinkedNode* node = new DLinkedNode(key,value);
chache[key] = node;
//将该节点添加到双向链表头部
addHead(node);
size++;
}
//如果key存在,通过哈希定位,修改value,移动到头部
else
{
DLinkedNode* node = chache[key];
node->value = value;
moveHead
}
}

双向指针的具体细节

添加到头节点函数

void addHead(DLinkedNode* node)
{
//双向指针操作 + 虚拟头节点
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}

删除尾节点函数

DLinkedNode* deleteTail()
{
//尾节点是虚拟tail前面一个。
DLinkedNode* node = tail->prev;
deleteNode(node);
return node;
}

删除节点函数

指针绕来绕去。。。面试没写出来。。。

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

删除节点函数

void moveHead(DLinkedNode* node)
{
//先删除当前节点
deleteNode(node);
//然后将它加入头部
addHead(node);
}

感想

第一次面试,还是太紧张了,说话都说不利索。
关键还是指针链表操作的繁琐。。。没想到面试会考这题。。
这一题和实际应用关联较大,而且是设计题,比较考研综合能力。
构造函数和双向链表的定义都得自己写,双向链表我平时用到的不是很多,还得多加练习。
为了面试,晚饭都没吃。。。
面试官声音好听,态度也很温和,像个邻家大哥哥,面完我还要再去面下一个,一个人一个小时,感觉也挺累的。


举报

相关推荐

0 条评论