LRU Cache
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get
and set
.
get(key)
- Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.set(key, value)
- Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
1)get(key):如果key在cache中,則返回對應的value值,否則返回-1
2)set(key,value):如果key不在cache中,則將該(key,value)插入cache中(注意,如果cache已滿,則必須把最近最久未使用的元素從cache中刪除);如果key在cache中,則重置value的值。
Recently Used,即最近最久未使用的意思。在操作系統的內存管理中,有一類很重要的算法就是內存頁面置換算法(包括FIFO,LRU,LFU等幾種常見頁面置換算法)。事實上,Cache算法和內存頁面置換算法的核心思想是一樣的:都是在給定一個限定大小的空間的前提下,設計一個原則如何來更新和訪問其中的元素。下面說一下LRU算法的核心思想,LRU算法的設計原則是:如果一個數據在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數據時,應當把最久沒有被訪問到的數據淘汰。
而用什么數據結構來實現LRU算法呢?可能大多數人都會想到:用一個數組來存儲數據,給每一個數據項標記一個訪問時間戳,每次插入新數據項的時候,先把數組中存在的數據項的時間戳自增,並將新數據項的時間戳置為0並插入到數組中。每次訪問數組中的數據項的時候,將被訪問的數據項的時間戳置為0。當數組空間已滿時,將時間戳最大的數據項淘汰。
這種實現思路很簡單,但是有什么缺陷呢?需要不停地維護數據項的訪問時間戳,另外,在插入數據、刪除數據以及訪問數據時,時間復雜度都是O(n)。
那么有沒有更好的實現辦法呢?
那就是利用鏈表和hashmap。當需要插入新的數據項的時候,如果新數據項在鏈表中存在(一般稱為命中),則把該節點移到鏈表頭部,如果不存在,則新建一個節點,放到鏈表頭部,若緩存滿了,則把鏈表最后一個節點刪除即可。在訪問數據的時候,如果數據項在鏈表中存在,則把該節點移到鏈表頭部,否則返回-1。這樣一來在鏈表尾部的節點就是最近最久未訪問的數據項。
總結一下:根據題目的要求,LRU Cache具備的操作:
1)set(key,value):如果key在hashmap中存在,則先重置對應的value值,然后獲取對應的節點cur,將cur節點從鏈表刪除,並移動到鏈表的頭部;若果key在hashmap不存在,則新建一個節點,並將節點放到鏈表的頭部。當Cache存滿的時候,將鏈表最后一個節點刪除即可。
2)get(key):如果key在hashmap中存在,則把對應的節點放到鏈表頭部,並返回對應的value值;如果不存在,則返回-1。
仔細分析一下,如果在這地方利用單鏈表和hashmap,在set和get中,都有一個相同的操作就是將在命中的節點移到鏈表頭部,如果按照傳統的遍歷辦法來刪除節點可以達到題目的要求么?第二,在刪除鏈表末尾節點的時候,必須遍歷鏈表,然后將末尾節點刪除,這個能達到題目的時間要求么?
試一下便知結果:
第一個版本實現:
#include <iostream> #include <map> #include <algorithm> using namespace std; struct Node { int key; int value; Node *next; }; class LRUCache{ private: int count; int size ; map<int,Node *> mp; Node *cacheList; public: LRUCache(int capacity) { size = capacity; cacheList = NULL; count = 0; } int get(int key) { if(cacheList==NULL) return -1; map<int,Node *>::iterator it=mp.find(key); if(it==mp.end()) //如果在Cache中不存在該key, 則返回-1 { return -1; } else { Node *p = it->second; pushFront(p); //將節點p置於鏈表頭部 } return cacheList->value; } void set(int key, int value) { if(cacheList==NULL) //如果鏈表為空,直接放在鏈表頭部 { cacheList = (Node *)malloc(sizeof(Node)); cacheList->key = key; cacheList->value = value; cacheList->next = NULL; mp[key] = cacheList; count++; } else //否則,在map中查找 { map<int,Node *>::iterator it=mp.find(key); if(it==mp.end()) //沒有命中 { if(count == size) //cache滿了 { Node * p = cacheList; Node *pre = p; while(p->next!=NULL) { pre = p; p= p->next; } mp.erase(p->key); count--; if(pre==p) //說明只有一個節點 p=NULL; else pre->next = NULL; free(p); } Node * newNode = (Node *)malloc(sizeof(Node)); newNode->key = key; newNode->value = value; newNode->next = cacheList; cacheList = newNode; mp[key] = cacheList; count++; } else { Node *p = it->second; p->value = value; pushFront(p); } } } void pushFront(Node *cur) //單鏈表刪除節點,並將節點移動鏈表頭部,O(n) { if(count==1) return; if(cur==cacheList) return; Node *p = cacheList; while(p->next!=cur) { p=p->next; } p->next = cur->next; //刪除cur節點 cur->next = cacheList; cacheList = cur; } void printCache(){ Node *p = cacheList; while(p!=NULL) { cout<<p->key<<" "; p=p->next; } cout<<endl; } }; int main(void) { /*LRUCache cache(3); cache.set(2,10); cache.printCache(); cache.set(1,11); cache.printCache(); cache.set(2,12); cache.printCache(); cache.set(1,13); cache.printCache(); cache.set(2,14); cache.printCache(); cache.set(3,15); cache.printCache(); cache.set(4,100); cache.printCache(); cout<<cache.get(2)<<endl; cache.printCache();*/ LRUCache cache(2); cout<<cache.get(2)<<endl; cache.set(2,6); cache.printCache(); cout<<cache.get(1)<<endl; cache.set(1,5); cache.printCache(); cache.set(1,2); cache.printCache(); cout<<cache.get(1)<<endl; cout<<cache.get(2)<<endl; return 0; }
提交之后,提示超時:
因此要對算法進行改進,如果把pushFront時間復雜度改進為O(1)的話是不是就能達到要求呢?
但是 在已知要刪除的節點的情況下,如何在O(1)時間復雜度內刪除節點?
如果知道當前節點的前驅節點的話,則在O(1)時間復雜度內刪除節點是很容易的。而在無法獲取當前節點的前驅節點的情況下,能夠實現么?對,可以實現的。
具體的可以參照這幾篇博文:
http://www.cnblogs.com/xwdreamer/archive/2012/04/26/2472102.html
http://www.nowamagic.net/librarys/veda/detail/261
原理:假如要刪除的節點是cur,通過cur可以知道cur節點的后繼節點curNext,如果交換cur節點和curNext節點的數據域,然后刪除curNext節點(curNext節點是很好刪除地),此時便在O(1)時間復雜度內完成了cur節點的刪除。
改進版本1:
void pushFront(Node *cur) //單鏈表刪除節點,並將節點移動鏈表頭部,O(1) { if(count==1) return; //先刪除cur節點 ,再將cur節點移到鏈表頭部 Node *curNext = cur->next; if(curNext==NULL) //如果是最后一個節點 { Node * p = cacheList; while(p->next!=cur) { p=p->next; } p->next = NULL; cur->next = cacheList; cacheList = cur; } else //如果不是最后一個節點 { cur->next = curNext->next; int tempKey = cur->key; int tempValue = cur->value; cur->key = curNext->key; cur->value = curNext->value; curNext->key = tempKey; curNext->value = tempValue; curNext->next = cacheList; cacheList = curNext; mp[curNext->key] = curNext; mp[cur->key] = cur; } }
提交之后,提示Accepted,耗時492ms,達到要求。
有沒有更好的實現辦法,使得刪除末尾節點的復雜度也在O(1)?那就是利用雙向鏈表,並提供head指針和tail指針,這樣一來,所有的操作都是O(1)時間復雜度。
改進版本2:
#include <iostream> #include <map> #include <algorithm> using namespace std; struct Node { int key; int value; Node *pre; Node *next; }; class LRUCache{ private: int count; int size ; map<int,Node *> mp; Node *cacheHead; Node *cacheTail; public: LRUCache(int capacity) { size = capacity; cacheHead = NULL; cacheTail = NULL; count = 0; } int get(int key) { if(cacheHead==NULL) return -1; map<int,Node *>::iterator it=mp.find(key); if(it==mp.end()) //如果在Cache中不存在該key, 則返回-1 { return -1; } else { Node *p = it->second; pushFront(p); //將節點p置於鏈表頭部 } return cacheHead->value; } void set(int key, int value) { if(cacheHead==NULL) //如果鏈表為空,直接放在鏈表頭部 { cacheHead = (Node *)malloc(sizeof(Node)); cacheHead->key = key; cacheHead->value = value; cacheHead->pre = NULL; cacheHead->next = NULL; mp[key] = cacheHead; cacheTail = cacheHead; count++; } else //否則,在map中查找 { map<int,Node *>::iterator it=mp.find(key); if(it==mp.end()) //沒有命中 { if(count == size) //cache滿了 { if(cacheHead==cacheTail&&cacheHead!=NULL) //只有一個節點 { mp.erase(cacheHead->key); cacheHead->key = key; cacheHead->value = value; mp[key] = cacheHead; } else { Node * p =cacheTail; cacheTail->pre->next = cacheTail->next; cacheTail = cacheTail->pre; mp.erase(p->key); p->key= key; p->value = value; p->next = cacheHead; p->pre = cacheHead->pre; cacheHead->pre = p; cacheHead = p; mp[cacheHead->key] = cacheHead; } } else { Node * p = (Node *)malloc(sizeof(Node)); p->key = key; p->value = value; p->next = cacheHead; p->pre = NULL; cacheHead->pre = p; cacheHead = p; mp[cacheHead->key] = cacheHead; count++; } } else { Node *p = it->second; p->value = value; pushFront(p); } } } void pushFront(Node *cur) //雙向鏈表刪除節點,並將節點移動鏈表頭部,O(1) { if(count==1) return; if(cur==cacheHead) return; if(cur==cacheTail) { cacheTail = cur->pre; } cur->pre->next = cur->next; //刪除節點 if(cur->next!=NULL) cur->next->pre = cur->pre; cur->next = cacheHead; cur->pre = NULL; cacheHead->pre = cur; cacheHead = cur; } void printCache(){ Node *p = cacheHead; while(p!=NULL) { cout<<p->key<<" "; p=p->next; } cout<<endl; } }; int main(void) { LRUCache cache(3); cache.set(1,1); //cache.printCache(); cache.set(2,2); //cache.printCache(); cache.set(3,3); cache.printCache(); cache.set(4,4); cache.printCache(); cout<<cache.get(4)<<endl; cache.printCache(); cout<<cache.get(3)<<endl; cache.printCache(); cout<<cache.get(2)<<endl; cache.printCache(); cout<<cache.get(1)<<endl; cache.printCache(); cache.set(5,5); cache.printCache(); cout<<cache.get(1)<<endl; cout<<cache.get(2)<<endl; cout<<cache.get(3)<<endl; cout<<cache.get(4)<<endl; cout<<cache.get(5)<<endl; return 0; }
提交測試結果:
可以發現,效率有進一步的提升。
其實在STL中的list就是一個雙向鏈表,如果希望代碼簡短點,可以用list來實現:
具體實現:
#include <iostream> #include <map> #include <algorithm> #include <list> using namespace std; struct Node { int key; int value; }; class LRUCache{ private: int maxSize ; list<Node> cacheList; map<int, list<Node>::iterator > mp; public: LRUCache(int capacity) { maxSize = capacity; } int get(int key) { map<int, list<Node>::iterator >::iterator it = mp.find(key); if(it==mp.end()) //沒有命中 { return -1; } else //在cache中命中了 { list<Node>::iterator listIt = mp[key]; Node newNode; newNode.key = key; newNode.value = listIt->value; cacheList.erase(listIt); //先刪除命中的節點 cacheList.push_front(newNode); //將命中的節點放到鏈表頭部 mp[key] = cacheList.begin(); } return cacheList.begin()->value; } void set(int key, int value) { map<int, list<Node>::iterator >::iterator it = mp.find(key); if(it==mp.end()) //沒有命中 { if(cacheList.size()==maxSize) //cache滿了 { mp.erase(cacheList.back().key); cacheList.pop_back(); } Node newNode; newNode.key = key; newNode.value = value; cacheList.push_front(newNode); mp[key] = cacheList.begin(); } else //命中 { list<Node>::iterator listIt = mp[key]; cacheList.erase(listIt); //先刪除命中的節點 Node newNode; newNode.key = key; newNode.value = value; cacheList.push_front(newNode); //將命中的節點放到鏈表頭部 mp[key] = cacheList.begin(); } } }; int main(void) { LRUCache cache(3); cache.set(1,1); cache.set(2,2); cache.set(3,3); cache.set(4,4); cout<<cache.get(4)<<endl; cout<<cache.get(3)<<endl; cout<<cache.get(2)<<endl; cout<<cache.get(1)<<endl; cache.set(5,5); cout<<cache.get(1)<<endl; cout<<cache.get(2)<<endl; cout<<cache.get(3)<<endl; cout<<cache.get(4)<<endl; cout<<cache.get(5)<<endl; return 0; }
本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。