1. 程式人生 > >C++實現快取演算法LRU和LFU

C++實現快取演算法LRU和LFU

LRU的實現

運用你所掌握的資料結構,設計和實現一個LRU (最近最少使用) 快取機制 。它應該支援以下操作: 獲取資料 get 和 寫入資料 put 。

獲取資料 get(key) - 如果金鑰 (key) 存在於快取中,則獲取金鑰的值(總是正數),否則返回 -1。
寫入資料 put(key, value) - 如果金鑰不存在,則寫入其資料值。當快取容量達到上限時,它應該在寫入新資料之前刪除最近最少使用的資料值,從而為新的資料值留出空間。

進階:

你是否可以在 O(1) 時間複雜度內完成這兩種操作?

思路:

LRU實現採用雙向連結串列 + hash_map 來進行實現。這裡採用雙向連結串列的原因是:如果採用普通的單鏈表,則刪除節點的時候需要從表頭開始遍歷查詢,效率為O(n),採用雙向連結串列可以直接改變節點的前驅的指標指向進行刪除達到O(1)的效率。使用map來儲存節點的key、value值便於能在O(logN)的時間查詢元素,對應get操作。

程式碼:

#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
#include<iostream>
#include<hash_map>
#include<algorithm>
using namespace std;
struct CacheNode {
	int key;
	int value;
	CacheNode *pre;
	CacheNode *next;
	CacheNode(int k, int v) :key(k), value(v), pre(nullptr), next(nullptr) {}
};

class LRUCache {
public:
	LRUCache(int capacity) :size(capacity), head(nullptr), tail(nullptr) {}
	int get(int key);
	void set(int key,int value);
	void remove(CacheNode *node);
	void SetHead(CacheNode *node);
	CacheNode *GetHead();
private:
	int size;//最大快取數
	CacheNode *head, *tail;
	hash_map<int, CacheNode *>map;//int是關鍵字
};
int LRUCache::get(int key)
{
	auto it = map.find(key);//先在hash表中查一下是否有該關鍵字
	//存在該關鍵字
	if (it != map.end())
	{
		CacheNode *node = it->second;
		//要獲取的關鍵字正好位於雙向連結串列的頭節點
		if (node == GetHead())
			return node->value;
		else
		{
			remove(node);
			SetHead(node);
		}
		return node->value;
	}
	else
		return -1;
}
void LRUCache::set(int key, int value)
{
	auto it = map.find(key);//先在hash表中查一下是否有該關鍵字
	//以前已經存在該關鍵字,需要修改值
	if (it != map.end())
	{
		CacheNode *node = it->second;
		//要獲取的關鍵字正好位於雙向連結串列的頭節點
		if (node == GetHead())
		{
			node->value = value;
			return;
		}
		node->value = value;
		remove(node);
		SetHead(node);
	}
	else
	{
		CacheNode *node = new CacheNode(key, value);
		if (map.size() == size)
		{
			auto it = map.find(tail->key);
			remove(tail);
			map.erase(it);
		}
		SetHead(node);
		map[key] = node;
	}
}
void LRUCache::remove(CacheNode *node)
{
	if (node->pre != nullptr)
		node->pre->next = node->next;
	else
		head = node->next;
	if (node->next != nullptr)
		node->next->pre = node->pre;
	else
		tail = node->pre;
}
void LRUCache::SetHead(CacheNode *node)
{
	if (head == nullptr)
	{
		head = node;
		tail = node;
	}
	else
	{
		head->pre = node;
		node->next = head;
		head = node;
	}
}
CacheNode* LRUCache::GetHead()
{
	return head;
}
int main(void)
{
	LRUCache *lruCache = new LRUCache(2);
	lruCache->set(1, 10);
	lruCache->set(2, 11);
	lruCache->get(1);
	lruCache->set(3, 12);
	cout << lruCache->get(1) << endl;
	cout << lruCache->get(2) << endl;
	system("pause");
	return 0;
}

LFU的實現

設計並實現最不經常使用(LFU)快取的資料結構。它應該支援以下操作:get 和 put

get(key) - 如果鍵存在於快取中,則獲取鍵的值(總是正數),否則返回 -1。
put(key, value) - 如果鍵不存在,請設定或插入值。當快取達到其容量時,它應該在插入新專案之前,使最不經常使用的專案無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,最近最少使用的鍵將被去除。

進階:
你是否可以在 O(1) 時間複雜度內執行兩項操作?

思路:

LRU實現採用2個雙向連結串列 + 2個hash_map 來進行實現。一個雙向連結串列用來連結各個頻率的連結串列,另一個連結串列用來連結相同訪問頻率的各個記錄。這兩個連結串列是巢狀關係。

兩個hash_map分別是存放關鍵字和對應的node,以及node所對應的頻率鏈。

程式碼:

#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
#include<iostream>
#include<hash_map>
#include<algorithm>
using namespace std;
class Node {
public:
	Node(int k, int v, int t) :key(k), value(v), times(t), pre(nullptr), next(nullptr) {}
public:
	int key;//關鍵字
	int value;//值
	int times;//訪問的次數
	Node *pre;//前一個節點
	Node *next;//後一個節點
};

class NodeList {
public:
	NodeList(Node *node);
	//每次新加入節點都是從頭部加入,所以對於
	//訪問次數相同的關鍵字,刪除的是連結串列尾部的那個
	void addNodeFromHead(Node *newHead);
	bool IsEmpty() { return head == nullptr; }
	void deleteNode(Node *node);
public:
	Node *head;
	Node *tail;
	NodeList *pre;
	NodeList *next;
};
NodeList::NodeList(Node *node)
{
	head = node;
	tail = node;
	pre = nullptr;
	next = nullptr;
}
void NodeList::addNodeFromHead(Node *newHead)
{
	newHead->next = head;
	head->pre = newHead;
	head = newHead;
}
void NodeList::deleteNode(Node *node)
{
	//刪掉這個節點之後,這個NodeList為空了
	if (head == tail)
	{
		head = nullptr;
		tail = nullptr;
	}
	//刪掉這個節點之後,連結串列中還至少有一個節點
	else
	{
		if (node == head)
		{
			head = node->next;
			head->pre = nullptr;
		}
		else if (node == tail)
		{
			tail = node->pre;
			tail->next = nullptr;
		}
		else
		{
			node->pre->next = node->next;
			node->next->pre = node->pre;
		}
	}
	node->next = nullptr;
	node->pre = nullptr;
}
class LFUCache {
public:
	LFUCache(int c);
	//加入一條記錄
	void set(int key, int value);
	//oldNodeList是Node原來屬於的那個NodeList,當訪問數加一之後需要找到下一個NodeList
	void move(Node *node, NodeList* oldNodeList); 
	//把一個node從lodeList裡面delete之後,然後呼叫此
	//方法進行判斷是不是連整個NodeList不要了,如果此NodeList不在了返回true
	bool modifyHeadList(NodeList *nodeList);
	//返回關鍵字對應的記錄
	int get(int key);
private:
	int capacity;//緩衝區容量
	int size;//目前緩衝區中的元素的數量
	hash_map<int, Node *> records;//關鍵字對應的節點構成一個map
	hash_map<Node *, NodeList *> heads;//每個節點對應一個NodeList,每個NodeList都是訪問次數相同的關鍵字
	NodeList *headList;//整個NodeList中的第一個
};
LFUCache::LFUCache(int c)
{
	capacity = c;
	size = 0;
	headList = nullptr;
}
void LFUCache::set(int key, int value)
{
	//說明緩衝區中有這個關鍵字
	auto it = records.find(key);
	if (it != records.end())
	{
		Node *node = it->second;
		node->value = value;//設定新的值
		node->times++;//訪問次數++
		NodeList *curNodeList = heads[node];//找到node對應的那個NodeList
		move(node, curNodeList);
	}
	else//緩衝區之前沒有這個關鍵字
	{
		if (size == capacity)//容量已經到達上限,需要刪除一個node
		{
			Node *node = headList->tail;//刪除的node是第一個NodeList的tail
			headList->deleteNode(node);
			modifyHeadList(headList);//這個NodeList是否需要調整
			records.erase(node->key);
			heads.erase(node);
			size--;
		}
		Node *node = new Node(key, value, 1);//新建一個節點,訪問次數為1 
		if (headList == nullptr)
			headList = new NodeList(node);
		else
		{
			if (headList->head->times == node->times)//如果當前的第一個NodeList正好就是node所對應的訪問次數
				headList->addNodeFromHead(node);//使用頭插法進行加入
			else//第一個NodeList所對應的訪問次數不是1,需要新建一個NodeList
			{
				NodeList *newList = new NodeList(node);
				newList->next = headList;
				headList->pre = newList;
				headList = newList;//更換新的headList
			}
		}
		records[key] = node;
		heads[node] = headList;
		size++;
	}
}
//oldNodeList是Node原來屬於的那個NodeList,當訪問數加一之後需要找到下一個NodeList
void LFUCache::move(Node *node, NodeList* oldNodeList)
{
	oldNodeList->deleteNode(node);//因為node對應的關鍵字的訪問次數加一,所以更換NodeList
	//如果刪除一個節點之後oldNodeList不存在了,那麼preList就是oldNodeList的前一個
	NodeList *preList = modifyHeadList(oldNodeList) ? oldNodeList->pre : oldNodeList;
	NodeList *nextList = oldNodeList->next;
	if (nextList == nullptr)//oldNodeList沒有連結串列了,那麼需要新建一個存放node
	{
		NodeList *newList = new NodeList(node);
		if (preList != nullptr)
			preList->next = newList;
		newList->pre = preList;
		if (headList == nullptr)
			headList = newList;
		heads[node] = newList;
	}
	else//oldNodeList有下一個節點
	{
		if (nextList->head->times == node->times)//正好訪問次數相等
		{
			nextList->addNodeFromHead(node);
			heads[node] = nextList;
		}
		else
		{
			NodeList *newList = new NodeList(node);
			if (preList != nullptr)
				preList->next = newList;
			newList->pre = preList;
			newList->next = nextList;
			nextList->pre = newList;
			if (headList == nextList)
				headList = newList;
			heads[node] = newList;
		}
	}
}

//把一個node從lodeList裡面delete之後,然後呼叫下面這個
//方法進行判斷是不是連整個連結串列不要了
bool LFUCache::modifyHeadList(NodeList *nodeList)
{
	//拿掉一個節點之後,nodeList為空
	if (nodeList->IsEmpty())
	{
		if (headList == nodeList)
		{
			headList = nodeList->next;
			if (headList != nullptr)
				headList->pre = nullptr;
		}
		else
		{
			nodeList->pre->next = nodeList->next;
			if (nodeList->next != nullptr)
				nodeList->next->pre = nodeList->pre;
		}
		return true;
	}
	return false;
}
int LFUCache::get(int key)
{
	auto it = records.find(key);
	if (it == records.end())
		return -1;
	Node *node = records[key];
	node->times++;
	NodeList *curNodeList = heads[node];
	move(node, curNodeList);
	return node->value;
}
int main(void)
{
	LFUCache *lfuCache = new LFUCache(2);
	lfuCache->set(1, 10);
	lfuCache->set(2, 11);
	lfuCache->get(1);
	lfuCache->get(2);
	lfuCache->get(1);
	lfuCache->get(2);
	lfuCache->set(3, 12);
	cout << lfuCache->get(1) << endl;
	cout << lfuCache->get(2) << endl;
	system("pause");
	return 0;
}