簡單的LRU Cache設計與實現
來自: http://www.cnblogs.com/zengzy/p/5167827.html
get(key):若緩存中存在key,返回對應的value,否則返回-1
set(key,value):若緩存中存在key,替換其value,否則插入key及其value,如果插入時緩存已經滿了,應該使用LRU算法把最近最久沒有使用的key踢出緩存。
設計1:
cache使用數組,每個key再關聯一個時間戳,時間戳可以直接用個long long類型表示,在cache中維護一個最大的時間戳:
- get的時候把key的時間戳變為最大時間戳+1
- set的時候, 數據從前往后存儲
如果key存在,更新key的時間戳為當前cache中最大的時間戳+1,并更新value;
如果key不存在,
若緩存滿,在整個緩存中查找時間戳最小的key,其存儲位置作為新key的存儲位置,設置key的時間戳為最大時間戳+1
若緩存未滿,設置key的時間戳為最大時間戳+1,存儲位置為第一個空閑位置
分析下時間空間復雜度,get的時候,需要從前往后找key,時間為O(N),set的時候,也要從前往后找key,當緩存滿的時候,還得找到時間戳最小的key,時間復雜度為O(N)。除了緩存本身,并沒有使用其他空間,空間復雜度為O(1)。 這個速度顯然是比較慢的,隨著數據量的增大,get和set速度越來越慢。可能有人會想到用哈希表作為底層存儲,這樣get的時間復雜度確實可以減低為O(1),set的時候,只要緩存沒有滿,也可以在O(1)的時間完,但在緩存滿的時候,依然需要每次遍歷找時間戳最小的key,時間復雜度還是O(N)。
設計2:
cache底層使用單鏈表,同時用一個哈希表存儲每個key對應的鏈表結點的前驅結點,并記錄鏈表尾結點的key
- get時,從哈希表中找到key對應的鏈表結點,挪到鏈表頭,更新指向尾結點的key
- set時,如果key存在,那么找到鏈表結點,并挪到鏈表頭,更新指向尾結點的key
如果key不存在,
若緩存滿,重用鏈表尾結點,設置新key和value,并挪到鏈表頭,更新指向尾結點的key
若緩存未滿,直接插入結點到鏈表頭,若是第一結點,更新指向尾結點的key
get,set時間復雜度O(1),總的空間復雜度O(N)。比前面的設計好一點。下面的再來看下關于設計2的兩個實現
實現1,自定義鏈表
為了方便鏈表的插入與刪除,使用了帶頭結點head的鏈表,所以真正有效的第一個結點是head->next。另外,只是簡單的實現,沒有容錯,不支持并發,簡單的內存管理
ps. 用雙向鏈表來實現會簡單寫,這里用單鏈表和哈希表共同實現了雙向鏈表的功效,也就是哈希除了用來查找,還指示了key對應的結點的前驅結點。
struct Node{ int _key; int _value; Node _next; Node(int key,int value,Node next):_key(key),_value(value),_next(next){} };class LRUCache{ public: LRUCache(int capacity) { _capacity = capacity; _size = 0; _last = 0; _cur_begin = _begin = (char ) malloc(sizeof(Node)(capacity+1)); _head = new (_cur_begin) Node(0,0,NULL);//在指定內存上構造對象 _cur_begin += sizeof(Node); }
~LRUCache(){ if(_begin!=NULL){ while(_cur_begin > _begin){ _cur_begin -= sizeof(Node); ((Node*)_cur_begin)->~Node();//先釋放內存上的對象 } free(_begin);//再釋放內存 } } int get(int key) { int value = -1;//初始時假設key對應的結點不存在 Node* pre_node_of_key = umap_prenodes[key];//key對應的結點的前驅結點 if(pre_node_of_key !=NULL){//key結點存在 Node* node = pre_node_of_key->_next;//key對應的結點 pre_node_of_key->_next = node->_next; if(pre_node_of_key->_next!=NULL){ umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key; } node->_next = _head->_next; if(node->_next!=NULL){//node有后繼,更新后繼的前驅結點 umap_prenodes[node->_next->_key] = node; } _head->_next = node; umap_prenodes[key] = _head; /*更新_last*/ if(_last == key ){ _last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); } value = node->_value; } return value; } void set(int key, int value) { Node* node = NULL; Node* pre_node_of_key = umap_prenodes[key];//key對應的結點的前驅結點 if(pre_node_of_key != NULL){//key對應的結點存在,孤立key對應的結點,也就是從鏈表中把結點取出來,重新鏈接鏈表 node = pre_node_of_key->_next;//key對應的結點 pre_node_of_key->_next = node->_next; if(pre_node_of_key->_next!=NULL){ umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;//更新前驅 } node->_value = value; //重置結點值 /*更新_last*/ if(_last == key ){ _last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); } }else{//結點不存在 if(_capacity == 0){//緩沖區為空 return ; } if(_size == _capacity){//緩存滿,重用最后一個結點 Node* pre_node_of_last = umap_prenodes[_last];//最后一個結點的前驅結點 umap_prenodes[pre_node_of_last->_next->_key] = NULL; node = new (pre_node_of_last->_next) Node(key,value,NULL);//重用最后一個結點 pre_node_of_last->_next = NULL;//移出最后一個結點 _last = ( pre_node_of_last == _head ? key : pre_node_of_last->_key ); //更新指向最后一個結點的key }else{//緩沖未滿,使用新結點 node = new (_cur_begin) Node(key,value,NULL); _cur_begin += sizeof(Node); _size++; if(_size==1){ _last = key; } } } /*把node插入到第一個結點的位置*/ node->_next = _head->_next; if(node->_next!=NULL){//node有后繼,更新后繼的前驅結點 umap_prenodes[node->_next->_key] = node; } _head->_next = node; umap_prenodes[key] = _head; }
private: int _size; int _capacity; int _last;//_last是鏈表中最后一個結點的key Node _head; unordered_map<int,Node> umap_prenodes;//存儲key對應的結點的前驅結點,鏈表中第一個結點的前驅結點為_head
char* _begin;//緩存的起始位置 char* _cur_begin;//用于分配結點內存的起始位置
};</pre>
實現2,使用stl的list
這個版本的實現來自 LeetCode discuss
class LRUCache{ size_t m_capacity; unordered_map<int, list<pair<int, int>>::iterator> m_map; //m_map_iter->first: key, m_map_iter->second: list iterator; list<pair<int, int>> m_list; //m_list_iter->first: key, m_list_iter->second: value; public: LRUCache(size_t capacity):m_capacity(capacity) { } int get(int key) { auto found_iter = m_map.find(key); if (found_iter == m_map.end()) //key doesn't exist return -1; m_list.splice(m_list.begin(), m_list, found_iter->second); //move the node corresponding to key to front return found_iter->second->second; //return value of the node } void set(int key, int value) { auto found_iter = m_map.find(key); if (found_iter != m_map.end()) //key exists { m_list.splice(m_list.begin(), m_list, found_iter->second); //move the node corresponding to key to front found_iter->second->second = value; //update value of the node return; } if (m_map.size() == m_capacity) //reached capacity { int key_to_del = m_list.back().first; m_list.pop_back(); //remove node in list; m_map.erase(key_to_del); //remove key in map } m_list.emplace_front(key, value); //create new node in list m_map[key] = m_list.begin(); //create correspondence between key and node } };通過兩個版本的實現,可以看到,使用stl的容器代碼非常簡潔,但也不是說自定義鏈表版本的實現就不好,如果從并發的角度來說,自定義的結構,在實現并發時,鎖的粒度會小一點,而直接使用stl容器,鎖的粒度為大一點,因為,使用stl,必須鎖定一個函數,而使用自定義結構可以只鎖定某個函數內部的某些操作,而且更方便實現無鎖并發。另外,從leetcode的測試結果來看,這兩個版本的性能差不多。
</div>