题目:
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
示例:
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得关键字 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得关键字 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lru-cache
思路:
设计的必要条件:
1.缓存中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素,腾出位置。
2.要在缓存中快速找某个key是否已存在并得到对应的value;
3.每次访问缓存中的某个key,需要将这个元素变为最近使用的,也就是说缓存要支持在任意位置快速插入和删除元素。
设计:
哈希表查找快,但是数据无固定顺序;双向链表有顺序之分,插入删除快,但是查找慢。所以结合形成一种新的数据结构:哈希链表。LRU缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。
每次默认从链表头部添加元素,那么越靠头部的元素就是最近使用的,越靠尾部的元素就是最久未使用的。
对于某一个key,可以通过哈希表快速定位到链表中的节点,从而取得对应value。
双向链表可以获得前驱指针,支持在任意位置快速插入和删除,但是链表无法按照索引快速访问某一个位置的元素,而借助哈希表,可以通过key快速映射到任意一个链表节点,然后进行插入和删除。
哈希表中已经存储key,链表中还要存储key的原因:当缓存已满,不仅要删除链表节点,还要把哈希表中映射到该节点的key同时删除,而这个key只能由链表节点得到,如果链表节点中只存储value,那么就无法得知key,就无法删除哈希表中的key。
Python解法:
1 class LRUCache: 2 3 def __init__(self, capacity: int): 4 self.dict = collections.OrderedDict() # 有序字典 5 self.remain = capacity 6 7 def get(self, key: int) -> int: 8 if key not in self.dict: return -1 9 v = self.dict.pop(key) # 取出该值并删除它 10 self.dict[key] = v # 将其重新放回缓存,且处于顶部(由于是有序字典) 11 return v 12 13 def put(self, key: int, value: int) -> None: 14 if key in self.dict: # 如果key存在,则应先将其value删除再更新为新值 15 self.dict.pop(key) 16 else: # 如果key不存在 17 # 缓存未满,放入新数值,容量减一 18 if self.remain > 0: self.remain -= 1 19 # 缓存已满,放入新数值,popitem返回并删除字典中的最后一对键和值 20 # Last=False,以队列方式弹出键值对,Last=True,以堆栈方式弹出键值对 21 else: self.dict.popitem(last = False) 22 self.dict[key] = value # 写入新数据 23 24 # Your LRUCache object will be instantiated and called as such: 25 # obj = LRUCache(capacity) 26 # param_1 = obj.get(key) 27 # obj.put(key,value)
C++解法:
1 struct DLinkedNode { 2 int key, value; // 双向链表的节点需要保存两个值 3 DLinkedNode *prev; 4 DLinkedNode *next; 5 /*=========================================================== 6 构造函数后面的冒号起分割作用,是类给成员变量赋值的方法,初始化列表, 7 更适用于成员变量是常量的const型 8 ===========================================================*/ 9 DLinkedNode(): key(0),value(0),prev(NULL),next(NULL) {} // 默认初始化 10 DLinkedNode(int _key, int _value): key(_key),value(_value),prev(NULL),next(NULL) {} 11 }; 12 13 class LRUCache { 14 private: 15 unordered_map<int, DLinkedNode*> cache; // 创建哈希表 16 DLinkedNode *head; 17 DLinkedNode *tail; 18 int size; // 缓存元素数量 19 int capacity; 20 21 public: 22 LRUCache(int _capacity):capacity(_capacity), size(0) { // 构造函数 23 head = new DLinkedNode(); // 哨兵头节点 24 tail = new DLinkedNode(); // 哨兵尾结点 25 head -> next = tail; 26 tail -> prev = head; 27 } 28 void addToHead(DLinkedNode *node) { // 元素添加至缓存头部 29 node -> prev = head; 30 node -> next = head -> next; 31 head -> next -> prev = node; 32 head -> next = node; 33 } 34 void removeNode(DLinkedNode *node) { // 删除缓存元素 35 node -> prev -> next = node -> next; 36 node -> next -> prev = node -> prev; 37 } 38 void moveToHead(DLinkedNode *node) { // 元素移至缓存头部 39 removeNode(node); 40 addToHead(node); 41 } 42 DLinkedNode *removeTail() { 43 DLinkedNode *delNode = tail -> prev; // 要被删除的链表尾节点 44 removeNode(delNode); 45 return delNode; // 由于还需要从哈希表中删除,故返回该节点 46 } 47 48 49 int get(int key) { 50 if(!cache.count(key)) // 如果要获取的元素不在缓存中 51 return -1; 52 else { 53 DLinkedNode *getNode = cache[key]; // 通过哈希表定位该key的链表节点 54 moveToHead(getNode); // 将访问的节点移至缓存头部 55 return getNode -> value; 56 } 57 } 58 59 void put(int key, int value) { 60 if(!cache.count(key)) { // 如果要添加的元素不在缓存中 61 DLinkedNode *newNode = new DLinkedNode(key, value); // 创建一个新节点 62 size++; 63 if(size > capacity) { // 如果超出容量 64 DLinkedNode *removedNode = removeTail(); // 从链表中删除尾节点 65 cache.erase(removedNode -> key); // 从哈希表中删除对应的项 66 delete removedNode; // 防止内存泄漏 67 size--; 68 } 69 cache[key] = newNode; // 新节点添加进哈希表中 70 addToHead(newNode); // 添加至链表头部,由于同一key,故可通过哈希表定位该节点 71 } 72 else { 73 DLinkedNode *yetNode = cache[key]; // 定位到该节点 74 yetNode -> value = value; // 跟新value 75 moveToHead(yetNode); // 将访问的元素移至缓存头部 76 } 77 } 78 }; 79 80 /** 81 * Your LRUCache object will be instantiated and called as such: 82 * LRUCache* obj = new LRUCache(capacity); 83 * int param_1 = obj->get(key); 84 * obj->put(key,value); 85 */