LFU缓存实现

Posted 六边形侠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LFU缓存实现相关的知识,希望对你有一定的参考价值。

LFU 缓存

回顾一下LRU:最近最久未使用。实现时采用双向链表+哈希表使得每次查询O1复杂度,哈希表中(key,双向链表节点(value)),查到之后,双向链表可以实现快速的删除操作,以及双向链表实现队列的效果。哈希就是为了get(key)快一些,而双向链表是为了put(key,value)的时候,调整顺序,以及满的时候淘汰,双向链表中存的只是value。

最不经常使用LFU:缓存满了之后,需要删除使用频率最小的内存,并且如果使用频率相同,需要删除插入时间最早的。

实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。
void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。

实现:双向链表+哈希表,原则:内存标记即key是唯一的,所以第一个哈希map:key--双向链表节点,第二个map:使用次数--第一次使用的节点

方法一:

用链表顺序记录是用最少的

#include<iostream>
#include<map>
#include<list>
#include<vector>
#include<unordered_map>
using namespace std;

/*
get:查询
put:插入、删除

哈希表,维护 key,node
哈希表,维护 cnt,firstnode  //cnt是使用次数,firstnode是最久未使用的

由于多个节点的cnt可能相同,因此记录每个cnt的最前方元素节点,保证最久不用的节点依次位于后方

链表node<cnt, value>
其中,cnt需要降序排列,
- 有新的key时,插入到cnt=1的最前面,保证最后一位是“最不常用&&cnt=1中最久没使用“的。
更新 cnt,node,firstnode
- 删除时,删除cnt最小的,即,最后一位
更新 cnt,node,firstnode
*/

class LFUCache {

private:
	int capacity;
	list<vector<int>> nodeList; //node<cnt, value, key>
	unordered_map<int, list<vector<int>>::iterator> keyNodeMap;
	unordered_map<int, list<vector<int>>::iterator> cntFirstNodeMap;

	//打印 list 和 map
	void printAllData(string action) {
		for (auto it = nodeList.begin(); it != nodeList.end(); it++) {
			auto vec = *it;
			cout << "{" << vec[2] << "," << vec[1] << "," << vec[0] << "},  ";
		}
		cout << endl;
		for (auto it = keyNodeMap.begin(); it != keyNodeMap.end(); it++) {
			int key = it->first;
			auto vec = *(it->second);
			cout << "key: " << key << "{" << vec[2] << "," << vec[1] << "," << vec[0] << "},  ";
		}
		cout << endl;
		for (auto it = cntFirstNodeMap.begin(); it != cntFirstNodeMap.end(); it++) {
			int cnt = it->first;
			auto vec = *(it->second);
			cout << "cnt: " << cnt << "{" << vec[2] << "," << vec[1] << "," << vec[0] << "},  ";
		}
		cout << endl << endl;
	}

public:
	LFUCache(int capacity) {
		this->capacity = capacity;
	}

	int get(int key) {
		if (!keyNodeMap.count(key)) return -1;
		auto currNode = keyNodeMap[key];
		int currCnt = (*currNode)[0];
		int value = (*currNode)[1];
		auto firstNode = cntFirstNodeMap[currCnt];
		int newCnt = currCnt + 1;
		//若newCnt已存在,则放到全部newCnt之前,否在放到全部currCnt之前
		if (cntFirstNodeMap.count(newCnt)) { firstNode = cntFirstNodeMap[newCnt]; }
		//更新list:插入
		auto newNode = nodeList.insert(firstNode, { newCnt, value, key });
		//更新keyNodeMap:替换
		keyNodeMap[key] = newNode;
		//新的cnt,更新cntFirstNodeMap:替换
		cntFirstNodeMap[newCnt] = newNode;
		//老的cnt,更新cntFirstNodeMap:若恰好为currNode,则更新or删除,否则不用动
		if (cntFirstNodeMap[currCnt] == currNode) {
			auto it = currNode;
			it++;
			if (it == nodeList.end()) {
				cntFirstNodeMap.erase(currCnt);
			}
			else if ((*it)[0] == currCnt) {
				cntFirstNodeMap[currCnt] = it;
			}
			else { //cnt变了,说明currCnt下,已经没有任何节点了
				cntFirstNodeMap.erase(currCnt);
			}
		}
		//更新list:删除
		nodeList.erase(currNode);
		//printAllData("get");
		return value;
	}

	void put(int key, int value) {
		if (capacity == 0) return;
		if (get(key) != -1) {
			(*keyNodeMap[key])[1] = value;
			//printAllData("put(update)");
			return;
		}
		//溢出删除
		if (nodeList.size() == capacity) {
			auto lastNode = --nodeList.end();
			int cnt = (*lastNode)[0];
			int value = (*lastNode)[1];
			int key = (*lastNode)[2];
			//keyNodeMap:删除这个key
			keyNodeMap.erase(key);
			//cntFirstNodeMap:相同则删除,否则不变
			if (cntFirstNodeMap[cnt] == lastNode) cntFirstNodeMap.erase(cnt);
			//nodeList:删除链表最后一个节点
			nodeList.pop_back();
		}
		//插入到 cnt=1 的最前面
		auto currNode = nodeList.end();
		if (cntFirstNodeMap.count(1)) {
			currNode = cntFirstNodeMap[1];
		}
		auto newNode = nodeList.insert(currNode, { 1, value, key });
		cntFirstNodeMap[1] = newNode;
		keyNodeMap[key] = newNode;
		//printAllData("put(add)");
		return;
	}
};

int main()
{
	//LFUCache a; 
	//定义了构造函数就没有默认构造函数里,在栈上创建出来的对象都有一个名字,比如 stu,使用指针指向它不是必须的。用.调用方法
	LFUCache *lFUCache = new LFUCache(2); //定义对象
	//通过 new 创建出来的对象就不一样了,它在堆上分配内存,没有名字,只能得到一个指向它的指针,所以必须使用一个指针变量来接收这个指针,否则以后再也无法找到这个对象了,更没有办法使用它。也就是说,使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。
	lFUCache->put(1, 1);  //因为上面定义的指针对象,所以->,不是指针对象用.
	lFUCache->put(2, 2);
	cout << lFUCache->get(1) << endl;  //key2对应的是1
	lFUCache->put(3, 3);
	cout << lFUCache->get(2) << endl;  //1已经被淘汰
	cout << lFUCache->get(1) << endl;
	system("pause");
}

方法二:

或是可以维护一个最小频次,每次移除一个节点的时候,需要移除最小频次的节点,每次增加时,也需要更新节点的频次。

#include<iostream>
#include<map>
#include<list>
#include<vector>
#include<unordered_map>
using namespace std;

/*
get:查询
put:插入、删除

哈希表,维护 key,node
哈希表,维护 cnt,firstnode  //cnt是使用次数,firstnode是最久未使用的

由于多个节点的cnt可能相同,因此记录每个cnt的最前方元素节点,保证最久不用的节点依次位于后方

这个链表不需要相连,只是用它的节点
维护一个最小频次
*/

struct Node
{
	int key;
	int value;
	int frenquency;
	Node(int _key, int _value, int _frequency) :key(_key), value(_value), frenquency(_frequency){}
};

class LFUCache {
public:
	LFUCache(int _capacity) {
		capacity = _capacity;
		minfreq = 0;
		frequency_table.clear();
		key_table.clear();
	}

	int get(int key) {
		if (capacity == 0) return -1;
		auto it = key_table.find(key);
		if (it == key_table.end()) return -1;
		auto node = it->second;
		int val = node->value;
		int freq = node->frenquency;
		//删除操作
		frequency_table[freq].erase(node);
		if (frequency_table[freq].size() == 0)
		{
			//记录freq频率的双链表没结点了
			frequency_table.erase(freq);
			if (minfreq == freq) minfreq++;
		}
		//添加结点
		frequency_table[freq + 1].push_front(Node(key, val, freq + 1));
		key_table[key] = frequency_table[freq + 1].begin();
		return val;
	}

	void put(int key, int value) {
		if (capacity == 0) return;
		auto it = key_table.find(key);
		//key表中找不到值,分缓存满和不满两种情况
		if (it == key_table.end())
		{
			//缓存已经满的情况
			if (key_table.size() == capacity)
			{
				auto it2 = frequency_table[minfreq].back();
				key_table.erase(it2.key);
				frequency_table[minfreq].pop_back();
				if (frequency_table[minfreq].size() == 0)
				{
					frequency_table.erase(minfreq);
				}
			}
			//两种情况都要添加操作,所以合并在一起
			frequency_table[1].push_front(Node(key, value, 1));
			key_table[key] = frequency_table[1].begin();
			minfreq = 1;
		}
		else{
			//如果表中存在,需要更新frequency的值
			auto node = it->second;
			int freq = node->frenquency;
			//删除操作
			frequency_table[freq].erase(node);
			if (frequency_table[freq].size() == 0)
			{
				//记录freq频率的双链表没结点了
				frequency_table.erase(freq);
				if (minfreq == freq) minfreq++;
			}
			//添加结点
			frequency_table[freq + 1].push_front(Node(key, value, freq + 1));
			key_table[key] = frequency_table[freq + 1].begin();
		}
	}

private:
	int minfreq;
	int capacity;
	unordered_map<int, list<Node>>frequency_table;
	unordered_map<int, list<Node>::iterator>key_table;
};

int main()
{
	//LFUCache a; 
	//定义了构造函数就没有默认构造函数里,在栈上创建出来的对象都有一个名字,比如 stu,使用指针指向它不是必须的。用.调用方法
	LFUCache *lFUCache = new LFUCache(2); //定义对象
	//通过 new 创建出来的对象就不一样了,它在堆上分配内存,没有名字,只能得到一个指向它的指针,所以必须使用一个指针变量来接收这个指针,否则以后再也无法找到这个对象了,更没有办法使用它。也就是说,使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。
	lFUCache->put(1, 1);  //因为上面定义的指针对象,所以->,不是指针对象用.
	lFUCache->put(2, 2);
	cout << lFUCache->get(1) << endl;  //key2对应的是1
	lFUCache->put(3, 3);
	cout << lFUCache->get(2) << endl;  //1已经被淘汰
	cout << lFUCache->get(1) << endl;
	system("pause");
}

以上是关于LFU缓存实现的主要内容,如果未能解决你的问题,请参考以下文章

缓存淘汰算法-LFU

LFU缓存

LFU缓存实现

-实现 LFU 缓存算法

左神算法进阶班6_1LFU缓存实现

leetcode困难460LFU 缓存