Leetcode: 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.
难度:90。这是一道非常综合的题目,主要应用在操作系统的资源管理中。
按照题目要求,要实现get和set功能,为了满足get功能:随机访问的需求(lookup)我们首先想到的一般是用数组,如果用链表会有O(n)的访问时间。然而他又有另一个条件就是要维护least used的队列,也就是说经常用的放在前面,用的少的放在后面。这样当资源超过cache的容积的时候就可以把用得最少的资源删掉。这就要求我们要对节点有好的删除和插入操作,这个要求又会让我们想到用链表,其删除插入是O(1),而数组的删除和插入是O(n)复杂度的。
lookup, insert operations in O(1) leads to use HashMap.
Hashtable也是一个不错的选择,本来它的insert, delete, lookup操作amortized代价都是O(1), 但是,这里delete无法O(1). 这是因为这里困难的是得找到合乎条件的节点去删除,需要花时间在search这个Least Recently Used节点。想过用一种方法,if we use a time stamp on each entry, deleting the one with oldest access time stamp, But it will take O(n) time cost. 所以,如果只用HashMap, 删除最老的节点需要O(n).
那么我们能不能维护一个数据结构使得访问操作和插入删除操作都是O(1)复杂度的呢?答案是肯定的。这个数据结构比较复杂,是用一个hash表加上一个双向链表来实现。
这里为什么使用Doubly linkd list 而不使用 Linked list的原因是:
当访问一个节点后,我们需要将他从原来的list中删除,然后将它插入到头节点处,删除过程中需要将其前后节点连接起来,单链表做不到。
查询:
- 根据键值查询hashmap,若命中,则返回节点,否则返回null。
- 从双向链表中删除命中的节点,将其重新插入到表头。
- 所有操作的复杂度均为O(1)。
插入:
- 将新的节点关联到Hashmap
- 如果Cache满了,删除双向链表的尾节点,同时删除Hashmap对应的记录
- 将新的节点插入到双向链表中头部
更新:
- 和查询相似
删除:
- 从双向链表和Hashmap中同时删除对应的记录。
通过hash表访问结点我们可以认为是O(1)的操作(假设hash函数足够好),然后双向链表的插入删除操作也是O(1)的操作。如此我们便实现了用O(1)时间来完成所有LRU cache的操作。空间上就是对于每一个资源有一个hash表的的项以及一个对应的结点(包含前后指针和资源的<key, value>)。代码如下:
1 public class LRUCache { 2 class Node { 3 Node prev, next; 4 int key; 5 int value; 6 public Node(int key, int value) { 7 this.key = key; 8 this.value = value; 9 } 10 } 11 12 int capacity; 13 int num; 14 Node first, last; 15 HashMap<Integer, Node> map; 16 17 public LRUCache(int capacity) { 18 this.capacity = capacity; 19 this.num = 0; 20 this.first = null; 21 this.last = null; 22 this.map = new HashMap<Integer, Node>(); 23 } 24 25 public int get(int key) { 26 Node n = map.get(key); 27 if (n == null) { //no find the key 28 return -1; 29 } 30 int val = n.value; 31 shiftToLast(n); 32 return val; //return the value of the key node 33 } 34 35 public void set(int key, int value) { 36 Node n = map.get(key); 37 if (n != null) { //find the node, need to change its value and put to last 38 shiftToLast(n); 39 n.value = value; //change the value 40 } 41 else { //not find the node, need to create one, put to the last 42 Node newNode = new Node(key, value); 43 44 if (num >= capacity) { //no capacity, need to delete first Node 45 map.remove(first.key); 46 first = first.next; 47 if (first != null) first.prev = null; 48 else last = null; 49 num--; 50 } 51 if (num == 0) { 52 first = newNode; 53 last = newNode; 54 } 55 else { 56 newNode.next = null; 57 newNode.prev = last; 58 last.next = newNode; 59 last = last.next; 60 } 61 num++; 62 map.put(key, newNode); 63 } 64 } 65 66 67 public void shiftToLast(Node n) { 68 if (n != last) { //find the key, and the node is not the last, need to put the node to the last of the LinkedList 69 if (n == first) { //delete the node from its original place, when the node is the first 70 first.next.prev = null; 71 first = first.next; 72 } 73 else { //delete the node from its original place, when the node is neither the first nor the last 74 n.next.prev = n.prev; 75 n.prev.next = n.next; 76 } 77 n.next = last.next; //put the node to the last of the Linkedlist 78 n.prev = last; 79 last.next = n; 80 last = n; 81 } 82 } 83 }
关于set()函数还有另一版写法,看哪个好理解
1 public void set(int key, int value) { 2 if (map.containsKey(key)) { // key exist, change the value of the corresponding node 3 CacheNode targetNode = map.get(key); 4 if (targetNode != last) { //if target node is not the last node, put it to the last 5 if (targetNode != first) { //if target node is not the first node, normal operation 6 targetNode.prev.next = targetNode.next; 7 targetNode.next.prev = targetNode.prev; 8 } 9 else { // if target node is the first node, special operation 10 targetNode.next.prev = targetNode.prev; 11 first = targetNode.next; 12 } 13 targetNode.next = last.next; 14 last.next = targetNode; 15 targetNode.prev = last; 16 last = last.next; 17 } 18 targetNode.val = value; 19 } 20 else { // key not exist, add the value to the LRUCache 21 CacheNode newNode = new CacheNode(key, value); 22 if (curnum < Capacity) { // LRU Capacity not reached, can add the node the LRU without deletion 23 if (last == null) { // LRU is initially empty, newNode is the first node added 24 last = newNode; 25 first = newNode; 26 } 27 else { // LRU is not initially empty, add newNode to the last 28 newNode.next = last.next; 29 last.next = newNode; 30 newNode.prev = last; 31 last = last.next; 32 } 33 curnum++; 34 map.put(key, newNode); 35 } 36 else { // curnum >= Capacity, LRU Capacity reached, deletes first node before adding newNode 37 if (first != null) { // Capacity is not 0, it Capacity is 0, do nothing, directly go to next if() 38 if (first == last) { // only one node in the LRU 39 map.remove(first.key); 40 first = null; 41 last = null; 42 } 43 else { //more than one node in the LRU, delete first node 44 first.next.prev = first.prev; 45 map.remove(first.key); 46 first = first.next; 47 } 48 curnum = Math.max(0, curnum-1); 49 } 50 if (curnum < Capacity) { //after deleting the firstNode, add the newNode to last.next 51 if (last == null) { // LRU is initially empty, newNode is the first node added 52 last = newNode; 53 first = newNode; 54 } 55 else { // LRU is not initially empty, add newNode to the last 56 newNode.next = last.next; 57 last.next = newNode; 58 newNode.prev = last; 59 last = last.next; 60 } 61 curnum++; 62 map.put(key, newNode); 63 } 64 } 65 } 66 }