功能
设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量 capacity
初始化 LRU 缓存
int get(int key)
如果关键字 key
存在于缓存中,则返回关键字的值,否则返回 -1
。
void put(int key, int value)
如果关键字 key
已经存在,则变更其数据值 value
;如果不存在,则向缓存中插入该组 key-value
。如果插入操作导致关键字数量超过 capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
思路
LRU
缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在O(1)
的时间内完成get
或者put
操作。具体的方法如下:
- 对于
get
操作,首先判断key
是否存在:
- 如果
key
不存在,则返回 −1;
- 如果
key
存在,则key
对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
- 对于
put
操作,首先判断key
是否存在:
- 如果
key
不存在,使用key
和value
创建一个新的节点,在双向链表的头部添加该节点,并将key
和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
- 如果
key
存在,则与get
操作类似,先通过哈希表定位,再将对应的节点的值更新为value
,并将该节点移到双向链表的头部。
上述各项操作中,访问哈希表的时间复杂度为O(1)
,在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为O(1)
。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在O(1)
时间内完成。
在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
演示
以下操作演示LRUCache
中数据的变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 输入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4]
解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); lRUCache.put(2, 2); lRUCache.get(1); lRUCache.put(3, 3); lRUCache.get(2); lRUCache.put(4, 4); lRUCache.get(1); lRUCache.get(3); lRUCache.get(4);
|
LRUCache lRUCache = new LRUCache(2)

lRUCache.put(1, 1)

lRUCache.put(2, 2)

lRUCache.get(1)
,返回1

lRUCache.put(3, 3)
,该操作会使得关键字2作废

lRUCache.get(2)
,返回-1(未找到)

lRUCache.put(4, 4)

lRUCache.get(1)
,返回-1(未找到)

lRUCache.get(3)
,返回3

lRUCache.get(4)
,返回4

代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| class LRUCache {
private final int capacity;
private final Node head;
private final Node tail;
private final HashMap<Integer, Node> map;
public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(); head = new Node(); tail = new Node(); head.next = tail; tail.pre = head; }
public int get(int key) { if (!map.containsKey(key)) { return -1; } Node node = map.get(key); moveNodeToHead(node); return node.value; }
private void moveNodeToHead(Node node) { deletedNode(node); addNodeToHead(node); }
private void deletedNode(Node node) { node.pre.next = node.next; node.next.pre = node.pre; }
private void addNodeToHead(Node node) { node.next = head.next; head.next.pre = node; node.pre = head; head.next = node; }
public void put(int key, int value) { if (map.containsKey(key)) { Node node = map.get(key); node.value = value; moveNodeToHead(node); } else { if (map.size() >= capacity) { Node preNode = tail.pre; map.remove(preNode.key); deletedNode(preNode); } Node node = new Node(key, value); map.put(key, node); addNodeToHead(node); } }
private class Node { public int key; public int value; public Node pre; public Node next;
public Node() { }
public Node(int key, int value) { this.key = key; this.value = value; } } }
|