設計並實現一個LRU Cache

1、什麼是Cache

1 概念

Cache,即高速緩存,是介於CPU和內存之間的高速小容量存儲器。在金字塔式存儲體系中它位於自頂向下的第二層,僅次於CPU寄存器。其容量遠小於內存,但速度卻能夠接近CPU的頻率。node

當CPU發出內存訪問請求時,會先查看 Cache 內是否有請求數據。ios

  • 若是存在(命中),則直接返回該數據;面試

  • 若是不存在(失效),再去訪問內存 —— 先把內存中的相應數據載入緩存,再將其返回處理器。算法

提供「高速緩存」的目的是讓數據訪問的速度適應CPU的處理速度,經過減小訪問內存的次數來提升數據存取的速度。緩存

2 原理

Cache 技術所依賴的原理是」程序執行與數據訪問的局部性原理「,這種局部性表如今兩個方面:網絡

  1. 時間局部性:若是程序中的某條指令一旦執行,不久之後該指令可能再次執行,若是某數據被訪問過,不久之後該數據可能再次被訪問。數據結構

  2. 空間局部性:一旦程序訪問了某個存儲單元,在不久以後,其附近的存儲單元也將被訪問,即程序在一段時間內所訪問的地址,可能集中在必定的範圍以內,這是由於指令或數據一般是順序存放的。函數

時間局部性是經過將近來使用的指令和數據保存到Cache中實現。空間局部性一般是使用較大的高速緩存,並將 預取機制 集成到高速緩存控制邏輯中來實現。this

3 替換策略

Cache的容量是有限的,當Cache的空間都被佔滿後,若是再次發生緩存失效,就必須選擇一個緩存塊來替換掉。經常使用的替換策略有如下幾種:spa

  1. 隨機算法(Rand):隨機法是隨機地肯定替換的存儲塊。設置一個隨機數產生器,依據所產生的隨機數,肯定替換塊。這種方法簡單、易於實現,但命中率比較低。

  2. 先進先出算法(FIFO, First In First Out):先進先出法是選擇那個最早調入的那個塊進行替換。當最早調入並被屢次命中的塊,極可能被優先替換,於是不符合局部性規律。這種方法的命中率比隨機法好些,但還不知足要求。

  3. 最久未使用算法(LRU, Least Recently Used):LRU法是依據各塊使用的狀況, 老是選擇那個最長時間未被使用的塊替換。這種方法比較好地反映了程序局部性規律。

  4. 最不常用算法(LFU, Least Frequently Used):將最近一段時期內,訪問次數最少的塊替換出Cache。

4 概念的擴充

現在高速緩存的概念已被擴充,不只在CPU和主內存之間有Cache,並且在內存和硬盤之間也有Cache(磁盤緩存),乃至在硬盤與網絡之間也有某種意義上的Cache──稱爲Internet臨時文件夾或網絡內容緩存等。凡是位於速度相差較大的兩種硬件之間,用於協調二者數據傳輸速度差別的結構,都可稱之爲Cache。


2、LRU Cache的實現

Google的一道面試題:

Design an LRU cache with all the operations to be done in O(1) .

1 思路分析

對一個Cache的操做無非三種:插入(insert)、替換(replace)、查找(lookup)。

爲了可以快速刪除最久沒有訪問的數據項和插入最新的數據項,咱們使用 雙向鏈表 鏈接Cache中的數據項,而且保證鏈表維持數據項從最近訪問到最舊訪問的順序。

  • 插入:當Cache未滿時,新的數據項只需插到雙鏈表頭部便可。時間複雜度爲O(1).

  • 替換:當Cache已滿時,將新的數據項插到雙鏈表頭部,並刪除雙鏈表的尾結點便可。時間複雜度爲O(1).

  • 查找:每次數據項被查詢到時,都將此數據項移動到鏈表頭部。

通過分析,咱們知道使用雙向鏈表能夠保證插入和替換的時間複雜度是O(1),但查詢的時間複雜度是O(n),由於須要對雙鏈表進行遍歷。爲了讓查找效率也達到O(1),很天然的會想到使用 hash table 。

2 代碼實現

從上述分析可知,咱們須要使用兩種數據結構:

  1. 雙向鏈表(Doubly Linked List)

  2. 哈希表(Hash Table)

下面是LRU Cache的 C++ 實現:

#include <iostream>
#include <unordered_map>
using namespace std;

// 雙向鏈表的節點結構
struct LRUCacheNode {
	int key;
	int value;
	LRUCacheNode* prev;
	LRUCacheNode* next;
	LRUCacheNode():key(0),value(0),prev(NULL),next(NULL){}
};


class LRUCache
{
private:
	unordered_map<int, LRUCacheNode*> m;  // 代替hash_map
	LRUCacheNode* head;     // 指向雙鏈表的頭結點
	LRUCacheNode* tail;     // 指向雙鏈表的尾結點
	int capacity;           // Cache的容量
	int count;              // 計數
public:
	LRUCache(int capacity);       // 構造函數
	~LRUCache();                  // 析構函數
	int get(int key);             // 查詢數據項
	void set(int key, int value); // 未滿時插入,已滿時替換
private:
	void removeLRUNode();                 // 刪除尾結點(最久未使用)
	void detachNode(LRUCacheNode* node);    // 分離當前結點
	void insertToFront(LRUCacheNode* node); // 節點插入到頭部
};


LRUCache::LRUCache(int capacity)
{
	this->capacity = capacity;
	this->count = 0;
	head = new LRUCacheNode;
	tail = new LRUCacheNode;
	head->prev = NULL;
	head->next = tail;
	tail->prev = head;
	tail->next = NULL;
}

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

int LRUCache::get(int key)
{
	if(m.find(key) == m.end())  // 沒找到
		return -1;
	else
	{
		LRUCacheNode* node = m[key];
		detachNode(node);      // 命中,移至頭部 
		insertToFront(node);
		return node->value;
	}
}

void LRUCache::set(int key, int value)
{
	if(m.find(key) == m.end())  // 沒找到
	{
		LRUCacheNode* node = new LRUCacheNode;
		if(count == capacity)   // Cache已滿
			removeLRUNode();

		node->key = key;
		node->value = value;
		m[key] = node;          // 插入哈希表
		insertToFront(node);    // 插入鏈表頭部
		++count;
	}
	else
	{
		LRUCacheNode* node = m[key];
		detachNode(node);
		node->value = value;
		insertToFront(node);
	}
}

void LRUCache::removeLRUNode()
{
	LRUCacheNode* node = tail->prev;
	detachNode(node);
	m.erase(node->key);
	--count;
}

void LRUCache::detachNode(LRUCacheNode* node)
{
	node->prev->next = node->next;
	node->next->prev = node->prev;
}


void LRUCache::insertToFront(LRUCacheNode* node)
{
	node->next = head->next;
	node->prev = head;
	head->next = node;
	node->next->prev = node;
}
相關文章
相關標籤/搜索