文章目录
- 1. 先验知识
- 1.1 时间相关
- 1.2 容器list 双向链表遍历删除
- 2. 超时清除
- 3. 最不经常使用LFU
- 方法2. LFU双向链表std::list解法
- 4. 经常使用LRU
- 方案一: 队列(差)
- 方案二: 双向链表(优秀)
- 本文主要实现不同cache, 是因为之前笔试中有个类似的问题需要实现, 并且在牛客上也有类似的训练题, 因此本文做个总结;
- cache中使用map作为key-value的存储, 在查询的时候速度会最快
- 十月一也没啥地去玩, 提高一下自己的业务能力吧
1. 先验知识
1.1 时间相关
#include <chrono> // 时间戳等头文件
#include <unistd.h> // sleep的头文件
using namespace std;
using namespace std::chrono;
void test(){
// 通过高精度时钟计算当前时间(毫秒表示),
// time_point<high_resolution_clock> m_begin = high_resolution_clock::now(); // 当前时间戳
auto m_begin = high_resolution_clock::now();
usleep(50000); // 最小单位是ns, 这里延时50ms
// sleep(1); // 注意这里是最小单位是s
int64_t ms = duration_cast<chrono::milliseconds>(high_resolution_clock::now() - m_begin).count(); // 计算过去了多长时间 ms单位
cout << ms <<endl;
cout << high_resolution_clock::now().time_since_epoch().count() <<endl; // 当前时间到1970年一月一日过去了多少ms
}
1.2 容器list 双向链表遍历删除
- 注意删除的时候一定要把堆上创建的内存delete一下
for(auto it=temp_time.begin(); it!=temp_time.end(); it++){
while(it!=temp_time.end() && 时间差 > m_time){
// 清除map中节点
temp_kv.erase((*it)->key);
// 清除list中节点
fd *p = *it;
it = temp_time.erase(it); // 在链表中删除, 并返回下一个指针
// 回收堆上
delete p;
m_capacity++;
}
}
2. 超时清除
设计个cache, 功能: 有最大cache空间, 最长保存时间; 当cache空间为0时, 不能写入数据, 数据超时后, 清除数据;
- set(int key, int value) 存在则更新key, 不存在则插入, 没空间则返回false
- get(int key) 存在则返回对应的value, 不存在则返回-1
- clean() 清除超时缓存
解决方案: 使用unorderd_map作为key,和fd的数据存储, 作为查询数据的返回空间, 使用list双向链表, 按照节点使用时间顺序保存, 超时清理时也是从头向后清理, 因为链表头是超时的, 链表尾是最新的;
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
#include <memory>
#include <chrono>
#include <unistd.h>
#include <unordered_map>
#include <queue>
using namespace std;
using namespace std::chrono;
// LRU设计1 超时清理
/*
1. 初始化时, 传入的参数 a.缓存空间最大长度, b.缓存的最长时间
2. set(key:int, value:int)->bool:已经存在(更新),未存在且插入成功(插入)
3. get(key:int) -> value, 没有返回-1
3. clean()->bool:清理所有超时的内容
对于超时清理解决:
struct fd{
int key;
int value;
timePoint;
};
map[fd.key] = fd
list使用介绍
维护一个链表, set或者get时, 更新timePoint, 从尾插入链表, 这样就是一个按照timePoint倒叙的链表
clean时, 从头遍历, 如果
*/
struct fd{
int key;
int value;
time_point<high_resolution_clock> timePoint=high_resolution_clock::now();
fd(int k, int v): key(k),value(v) {}
};
class TimeoutCache{
public:
TimeoutCache(int capacity, int t):m_capacity(capacity),m_time(t){}
~TimeoutCache(){
cout << "析构开始" <<endl;
for(auto &a : temp_time){
delete a;
}
cout << "析构成功" <<endl;
}
// 存在:
// 不存在: 判断capacity
bool set(int k, int v){
cout << "set: ";
if(temp_kv.count(k) == 1) { // key存在这里, 更新返回
cout << k << "存在, 历史值:" << temp_kv[k]->value << "更新为:" <<v << endl;
temp_kv[k]->value = v;
update_temp_time(k);
} else if(m_capacity != 0){ // key没在缓存, 并且有空间 插入缓存
m_capacity--;
fd * newFd = new fd(k, v);
temp_kv[k] = newFd;
insert_temp_time(newFd);
cout << k << "不存在 插入值" << temp_kv[k]->value << endl;
} else{ // 没空间
cout << "空间不足" << k << "插入" << v << "失败" <<endl;
return false;
}
return true;
}
// 更新时间为当前时间, 并且将节点放到最尾的位置
void update_temp_time(int key){
fd *p;
for(auto it=temp_time.begin(); it!=temp_time.end(); it++){
if((*it)->key == key){
p = *it;
temp_time.erase(it);
break;
}
}
cout << "提前了: " << p->key << " " << p->value <<endl;
p->timePoint = high_resolution_clock::now();
temp_time.push_back(p);
}
// temp_time头插节点t
void insert_temp_time(fd *t){
temp_time.push_back(t);
}
// 得到key对应的value, 并且更新或者提前
int get(int key){
cout << "get: ";
if(temp_kv.count(key) == 1){
cout << key << " " << temp_kv[key]->value <<endl;
update_temp_time(key);
return temp_kv[key]->value;
}else{
cout << key << "不存在" << endl;
return -1;
}
}
// 清除所有超时的节点
void clean(){
cout << "clean: " <<endl;
time_point<high_resolution_clock> now = high_resolution_clock::now();
for(auto it=temp_time.begin(); it!=temp_time.end(); it++){
while(it!=temp_time.end() && duration_cast<chrono::milliseconds>(now - (*it)->timePoint).count() > m_time){
// 清除map中节点
cout << (*it)->key << "在temp_kv已经清除, 因为时间到了.. " <<endl;
temp_kv.erase((*it)->key);
// 清除list中节点
fd *p = *it;
it = temp_time.erase(it); // 在链表中删除, 并返回下一个指针
// 回收堆上
delete p;
m_capacity++;
}
}break;
}
private:
unordered_map<int, fd*> temp_kv;
list<fd*> temp_time; // 负责超时删除, 更新重排
int m_capacity; // 最大容量
int m_time; // 最大存在时间
};
void test(){
TimeoutCache *myCache = new TimeoutCache(3, 50); // 3个空间, 4个时间长
myCache->get(1);
myCache->set(1,1);
myCache->set(1,13);
myCache->set(2,435);
myCache->set(3,32);
usleep(40000);
myCache->set(1,32);
usleep(30000); // 100毫秒过去了,么得了
myCache->clean(); // 这里应该清除23, 保留1
myCache->set(5,1);
myCache->get(1);
myCache->set(8,1);
delete myCache;
}
int main(int argc, char const *argv[]){
test();
return 0;
}
(base) zjq@DESKTOP-82TMKG6:LeetCode101$ ./"test"
get: 1不存在
set: 1不存在 插入值1
set: 1存在, 历史值:1更新为:13
提前了: 1 13
set: 2不存在 插入值435
set: 3不存在 插入值32
set: 1存在, 历史值:13更新为:32
提前了: 1 32
clean:
2在temp_kv已经清除, 因为时间到了..
3在temp_kv已经清除, 因为时间到了..
set: 5不存在 插入值1
get: 1 32
提前了: 1 32
set: 8不存在 插入值1
析构开始
析构成功
3. 最不经常使用LFU
LeetCode跳转
定义两个哈希表,第一个 freq_table 以频率 freq 为索引,每个索引存放一个双向链表,这个链表里存放所有使用频率为 freq 的缓存,缓存里存放三个信息,分别为键 key,值 value,以及使用频率 freq。第二个 key_table 以键值 key 为索引,每个索引存放对应缓存在 freq_table 中链表里的内存地址,这样我们就能利用两个哈希表来使得两个操作的时间复杂度均为 O(1)O(1)。同时需要记录一个当前缓存最少使用的频率 minFreq,这是为了删除操作服务的。
// 缓存的节点信息
struct Node {
int key, val, freq;
Node(int _key,int _val,int _freq): key(_key), val(_val), freq(_freq){}
};
class LFUCache {
int minfreq, capacity;
unordered_map<int, list<Node>::iterator> key_table;
unordered_map<int, list<Node>> freq_table;
public:
LFUCache(int _capacity) {
minfreq = 0;
capacity = _capacity;
key_table.clear();
freq_table.clear();
}
int get(int key) {
if (capacity == 0) return -1;
auto it = key_table.find(key);
if (it == key_table.end()) return -1;
list<Node>::iterator node = it -> second;
int val = node -> val, freq = node -> freq;
freq_table[freq].erase(node);
// 如果当前链表为空,我们需要在哈希表中删除,且更新minFreq
if (freq_table[freq].size() == 0) {
freq_table.erase(freq);
if (minfreq == freq) minfreq += 1;
}
// 插入到 freq + 1 中
freq_table[freq + 1].push_front(Node(key, val, freq + 1));
key_table[key] = freq_table[freq + 1].begin();
return val;
}
void put(int key, int value) {
if (capacity == 0) return;
auto it = key_table.find(key);
if (it == key_table.end()) {
// 缓存已满,需要进行删除操作
if (key_table.size() == capacity) {
// 通过 minFreq 拿到 freq_table[minFreq] 链表的末尾节点
auto it2 = freq_table[minfreq].back();
key_table.erase(it2.key);
freq_table[minfreq].pop_back();
if (freq_table[minfreq].size() == 0) {
freq_table.erase(minfreq);
}
}
freq_table[1].push_front(Node(key, value, 1));
key_table[key] = freq_table[1].begin();
minfreq = 1;
} else {
// 与 get 操作基本一致,除了需要更新缓存的值
list<Node>::iterator node = it -> second;
int freq = node -> freq;
freq_table[freq].erase(node);
if (freq_table[freq].size() == 0) {
freq_table.erase(freq);
if (minfreq == freq) minfreq += 1;
}
freq_table[freq + 1].push_front(Node(key, value, freq + 1));
key_table[key] = freq_table[freq + 1].begin();
}
}
};
方法2. LFU双向链表std::list解法
解题思路:
- 最长不使用
LFU
- 每个节点都有个计数器, 每次put或者get都对应+1
- 删除的时候是根据使用频率低的删除, 因此使用min_作为频率最低的标记
-
temp_
存储的是<key, {key, value, freq}>
-
freqMap
存储的是<freq, list<{key, value, freq}>>
所以每次删除都是删除freq最小的list
里面的front
, 因为新节点是push_back
后插
注意
由于每次需要从freqMap
中取出对应freq
的list
, 修改这个list
, 因此需要在取出的过程使用&
作为引用, 不然修改list
也没有修改freqMap
的数据;
#include <list>
#include <unordered_map>
#include <iostream>
using namespace std;
struct Node{
int key, value;
int freq = 1;
Node(int k, int v): key(k), value(v){}
};
class LFUCache {
private:
unordered_map<int, Node*> temp_;
// 存储每个频次对应的双向链表 <频次, 相同频次的节点双向链表
unordered_map<int, list<Node*>> freqMap_;
int size_;
int capacity_;
int min_; // 当前最小频次
public:
LFUCache(int capacity) : capacity_(capacity), size_(0), min_(0){
}
int get(int key) {
if(temp_.count(key) == 0){
return -1;
}
freqInc(temp_[key]);
return temp_[key]->value;
}
void put(int key, int value) {
if(temp_.count(key) != 0){
temp_[key]->value = value;
freqInc(temp_[key]);
} else {
// 删掉一个缓存
if(size_ >= capacity_){
Node *deadNode = removeNode();
temp_.erase(deadNode->key);
size_--;
}
Node *newNode = new Node(key, value);
temp_[key] = newNode;
addNode(newNode);
size_++;
}
}
// 频率计数器, 取出节点, 在频率+1后放到对应的频率里面
void freqInc(Node* node){
// 在频率的双向链表中删除, 更新min
int freq = node->freq;
// list<Node*> listN = freqMap_[freq]; // 这里是深拷贝给listN而不是把指针给他了
list<Node*> &listN = freqMap_[freq]; // 引用
listN.remove(node);
if(freq == min_ && listN.size()==0){
min_ = freq+1;
}
// 加入新freq对应链表 (freq+1)的链表
node->freq++;
if(freqMap_.count(node->freq) == 0){
list<Node*> listN;
listN.push_back(node);
freqMap_[node->freq] = listN;
}
freqMap_[node->freq].push_back(node);
return;
}
// 增加节点
void addNode(Node *node){
// 如果没有次数为1的, 创建双向链表, 加入到频率map中
if(freqMap_.count(1) == 0){
list<Node*> listN;
freqMap_[1] = listN;
}
freqMap_[1].push_back(node);
min_ = 1;
return;
}
// 移除节点
Node *removeNode(){
// 注意这里一定要使用引用, 不然修改list并没有修改freqMap_里面的数据
list<Node*> &minList = freqMap_[min_];
Node* deadNode = minList.front();
minList.pop_front();
return deadNode;
}
void printTemp(){
for(auto &a : temp_){
cout<< a.first << " " << a.second->value << " " << a.second->freq << "; ";
}cout << endl;
}
};
/**
* Your LFUCache object will be instantiated and called as such:
* LFUCache* obj = new LFUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
int main(int argc, char const *argv[])
{
LFUCache *lFUCache = new LFUCache(2);
lFUCache->put(1, 1); // cache=[1,_], cnt(1)=1
lFUCache->put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1
lFUCache->printTemp();
lFUCache->get(1); // 返回 1
lFUCache->printTemp();
// cache=[1,2], cnt(2)=1, cnt(1)=2
lFUCache->put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
lFUCache->printTemp();
// cache=[3,1], cnt(3)=1, cnt(1)=2
lFUCache->get(2); // 返回 -1(未找到)
lFUCache->get(3); // 返回 3
lFUCache->printTemp();
// cache=[3,1], cnt(3)=2, cnt(1)=2
lFUCache->put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
lFUCache->printTemp();
// cache=[4,3], cnt(4)=1, cnt(3)=2
lFUCache->get(1); // 返回 -1(未找到)
lFUCache->get(3); // 返回 3
// cache=[3,4], cnt(4)=1, cnt(3)=3
lFUCache->get(4); // 返回 4
// cache=[3,4], cnt(4)=2, cnt(3)=3
return 0;
}
4. 经常使用LRU
设计LRU缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能 set(key, value):将记录(key, value)插入该结构 get(key):返回key对应的value值 [要求] set和get方法的时间复杂度为O(1) 某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的。 当缓存的大小超过K时,移除最
不经常使用的记录,即set或get最久远的。 若opt=1,接下来两个整数x, y,表示set(x, y) 若opt=2,接下来一个整数x,表示get(x),若x未出现过或已被移除,则返回-1 对于每个操作2,输出一个答案
方案一: 队列(差)
解题思路:
- 使用unordered_map存储数据
- 使用list作为queue的存储最常使用放到队列尾部, 也就是list头部
- 每次set或者get一个值,先从queue中拿出来, 在放到queue头部, 这样就保证每次处理key , 都能将key置前了
输入:[[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
返回值:[1,-1]
说明:
第一次操作后:最常使用的记录为("1", 1)
第二次操作后:最常使用的记录为("2", 2),("1", 1)变为最不常用的
第三次操作后:最常使用的记录为("3", 2),("1", 1)还是最不常用的
第四次操作后:最常用的记录为("1", 1),("2", 2)变为最不常用的
第五次操作后:大小超过了3,所以移除此时最不常使用的记录("2", 2),加入记录("4", 4),并且为最常使用的记录,然后("3", 2)变为最不常使用的记录
#include <iostream>
#include <vector>
#include <algorithm> // reverse
#include <sstream> // 为了拆解单词
#include <string> // 为了getline带空格的字符串
#include <unordered_map>
#include <list>
using namespace std;
struct Node{
int key, val;
Node(int k, int v):key(k),val(v){}
};
class Solution{
list<Node> queue_node;
// vector<Node> queue_node;
unordered_map<int, list<Node>::iterator> map_node; // 这里的map'val是Node的迭代器, 用来保存key, 和迭代器来直接找到queue
int k;
public:
Solution(){}
// 删除
int remove(list<Node>::iterator &ite){
int k=ite->key;
int v=ite->val;
queue_node.erase(ite);
// queue_node.erase(ite);
map_node.erase(k);
return v;
}
// 给队列和map添加值
void add(int key, int val){
// cout<< queue_node.begin()->key<<endl;
// if(queue_node.size()==0) queue_node.push_back(Node(key, val));
// else queue_node.insert(queue_node.begin(), Node(key, val));
// cout<< queue_node.begin()->key<<endl;
queue_node.push_front(Node(key, val));
map_node[key] = queue_node.begin(); // 相当于把当前这个节点的指针保存到了map中
if(queue_node.size() > this->k){
auto last_node = queue_node.end(); // 得到最后一位的下一位 xxx#的#
--last_node;
remove(last_node);
}
}
// set
void set(int key, int val){
auto ite = map_node.find(key); // 判断当前key在不在
if(ite != map_node.end()) remove(ite->second); // 找到了则需要删除在重新在头插入
add(key, val);
}
int get(int key){
auto ite = map_node.find(key);
if(ite != map_node.end()){ // 如果当前key已经在map里面, 则删除map在重新插入, 以此提前
int val = remove(ite->second);
add(key, val);
return val;
}
return -1;
}
vector<int> LRU(vector<vector<int>> &operators, int k)
{
// write code here
this->k = k;
vector<int> ans;
for (auto &input : operators)
{
if (input[0] == 1)
{
set(input[1], input[2]);
}
else if(input[0] == 2)
{
ans.push_back(get(input[1]));
}
}
return ans;
}
};
int main(){
vector<vector<int>> input = {{1,1,1},{1,2,2},{1,3,2},{2,1},{1,4,4},{2,2}};
// Solution s = new Solution();
Solution s;
s.LRU(input, 3);
return 0;
}
方案二: 双向链表(优秀)
方案一中, 使用队列进行排序, 但是这样做需要每次都要遍历一遍队列, 而如果利用双向链表作为节点存储, 就可以直接将其中的一个node
提前或者删除
解题思路:
- 创建双向链表, 其中的key和value对应cache里面的key和value
- 双向链表的方法包括
- 把一个节点放到双向链表的前面
head
←→node
←→head→next
- 把末尾的节点删除
node→prev
←→node
←→tail
转为 node→prev
←→tail
- 移除节点 直接前驱跟后继向连
- 移动节点到头部 (先移除节点3, 然后将节点加头部1)
// 双向链表
struct DoubleLinkedNode {
int key, value;
DoubleLinkedNode *prev;
DoubleLinkedNode *next;
DoubleLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr){}
DoubleLinkedNode(int k, int v):key(k), value(v), prev(nullptr), next(nullptr){}
};
// 每个cache保存的是双向链表的节点, temp[key] = DoubleLinkedNode(key, value)
// 这样就可以在get的时候, 将节点key, 从双向链表中, 放到最前面
// 在put时, 将节点key, 直接放到双向链表前面
class LRUCache {
private:
unordered_map<int, DoubleLinkedNode*> temp_;
int capacity_;
DoubleLinkedNode *head_ = nullptr;
DoubleLinkedNode *tail_ = nullptr;
int size_;
public:
LRUCache(int capacity) : capacity_(capacity), size_(0) {
// 创建个伪头和伪尾部节点
// head_(0,0)←→tail_(0,0)
this->head_ = new DoubleLinkedNode();
this->tail_ = new DoubleLinkedNode();
this->head_->next = this->tail_;
this->tail_->prev = this->head_;
}
int get(int key) {
if(temp_.count(key) == 0){
return -1;
}else{
// 更新key
DoubleLinkedNode* node = temp_[key];
moveTohead(node);
return node->value;
}
}
void put(int key, int value) {
if(temp_.count(key) == 0){
DoubleLinkedNode* node = new DoubleLinkedNode(key, value);
temp_[key] = node;
addToHead(node);
size_++;
if(size_ > capacity_){
DoubleLinkedNode* removedNode = removeTail();
temp_.erase(removedNode->key);
delete removedNode;
size_--;
}
}else{
DoubleLinkedNode* node = temp_[key];
node->value = value;
moveTohead(node);
}
}
// 节点加到最前面
void addToHead(DoubleLinkedNode* node){
// head_ ←→ node ←→ head_->next
node->prev = head_;
node->next = head_->next;
head_->next->prev = node;
head_->next = node;
}
// 移除节点
void removeNode(DoubleLinkedNode *node){
node->prev->next = node->next;
node->next->prev = node->prev;
}
// 删除尾部节点
DoubleLinkedNode *removeTail(){
DoubleLinkedNode *node = tail_->prev;
removeNode(node);
return node;
}
// 移动节点到头
void moveTohead(DoubleLinkedNode *node){
removeNode(node);
addToHead(node);
}
};
/**
* 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);
*/