1 题目描述
请你设计并实现一个满足 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) 的平均时间复杂度运行。
🌸「示例:」
输入
["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]
2 解答
解法一:自己造轮子
🌸 「分析一下:」
- 构建双链表的节点类
Node - 依靠
Node类构建一个双链表,实现几个LRU算法必须的API- 在链表尾部添加节点 x,时间
O(1) - 删除链表中的 x 结点,时间
O(1) - 删除链表中第一个结点,并返回该节点,时间
O(1) - 返回链表长度,时间
O(1)
- 在链表尾部添加节点 x,时间
注意我们实现的双链表 API 只能从尾部插⼊,也就是说靠尾部的数据是最近使⽤的,靠头部的数据是最久为使⽤的。
- 有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可
- 尽量让 LRU 的主⽅法
get和put避免直接操作map和cache的细节。我们可以先实现下⾯⼏个函数- 将某个 key 提升为最近使用的
makeRecently(int key) - 添加最近使用的元素
addRecently(int key, int val) - 删除某一个 key (非必要)
- 删除最久未使用的元素
removeLeastRecently()
- 将某个 key 提升为最近使用的
最后
-
get(int key) -
put(int key, int value)
🌸 「实现:」
public class M146 {
public static void main(String[] args) {
//["LRUCache","put","put","put","put","get","get"]
//[[2],[2,1],[1,1],[2,3],[4,1],[1],[2]]
LRUCache lruCache = new LRUCache(2);
lruCache.put(2, 1);
lruCache.put(1, 1);
lruCache.put(2, 3);
lruCache.put(4, 1);
lruCache.get(1);
lruCache.get(2);
HashMap<Integer, Node> map = lruCache.map;
System.out.println(map.keySet());
}
}
// LRU 缓存算法的核⼼数据结构就是哈希链表,双向链表和哈希表的结合体。
class Node{
public int key, val;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
public Node next;
public Node prev;
}
class DoubleList{
// 头尾虚结点
private Node head, tail;
// 链表元素数
private int size;
public DoubleList() {
head = new Node(0, 0);
tail = new Node(0, 0);
size = 0;
head.next = tail;
tail.prev = head;
}
// 在链表尾部添加节点 x,时间 O(1)
public void addLast(Node node) {
node.prev = tail.prev;
node.next = tail;
tail.prev.next = node;
tail.prev = node;
size++;
}
// 删除链表中的 x 节点(x ⼀定存在)
// 由于是双链表且给的是⽬标 Node 节点,时间 O(1)
public void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 删除链表中第⼀个节点,并返回该节点,时间 O(1)
public Node removeFirst() {
if (head.next == null) return null;
Node first = head.next;
remove(first);
return first;
}
// 返回链表长度,时间 O(1)
public int size(){
return size;
}
}
class LRUCache {
// key -> Node(key, val)
public HashMap<Integer, Node> map;
// Node(k1, v1) <-> Node(k2, v2)...
public DoubleList cache;
// 最⼤容量
private int cap;
// ⾸先要接收⼀个 capacity 参数作为缓存的最⼤容量,然后实现两个 API
public LRUCache(int capacity) {
this.cap = capacity;
map = new HashMap<>();
cache = new DoubleList();
}
/* 将某个 key 提升为最近使⽤的 */
private void makeRecently(int key) {
Node x = map.get(key);
// 先从链表中删除这个结点
cache.remove(x);
// 重新插到队尾
cache.addLast(x);
}
/* 添加最近使⽤的元素 */
private void addRecently(int key, int val) {
Node x = new Node(key, val);
cache.addLast(x);
// 别忘了在 map 中添加 key 的映射
map.put(key, x);
}
/* 删除某⼀个 key */
private void deleteKey(int key) {
Node x = map.get(key);
// 从链表中删除
cache.remove(x);
// 从 map 中删除
map.remove(key);
}
/* 删除最久未使⽤的元素 */
private void removeLeastRecently() {
// 链表头部的第⼀个元素就是最久未使⽤的
Node x = cache.removeFirst();
// 同时别忘了从 map 中删除它的 key
map.remove(x.key);
}
// 另⼀个是 get(key) ⽅法获取 key 对应的 val,如果 key 不存在则返回 -1。
public int get(int key) {
if (!map.containsKey(key)){
return -1;
}
Node x = map.get(key);
makeRecently(x.key);
return map.get(key).val;
}
// ⼀个是 put(key, val) ⽅法存 ⼊键值对
// 较复杂
public void put(int key, int value) {
// 若key 已存在
if (map.containsKey(key)){
//// 删除旧的数据
//deleteKey(key);
//// 新插⼊的数据为最近使⽤的数据
//addRecently(key, value);
//return;
// 修改值
map.get(key).val = value;
// 将key 提升为最近使用
makeRecently(key);
return;
}
if (cap == cache.size()){
// 淘汰最久未使用的key
removeLeastRecently();
}
addRecently(key, value);
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
解法二:使用 LinkedHashMap
🌸 「实现:」
class LRUCache2 {
int cap;
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
private void makeRecently(int key) {
Integer value = cache.get(key);
//System.out.println("makeRecently.key=" + value);
// 删除key,重新插入到队尾
cache.remove(key);
cache.put(key, value);
}
public LRUCache2(int capacity) {
this.cap = capacity;
}
public int get(int key) {
if (!cache.containsKey(key)){
return -1;
}
makeRecently(key);
return cache.get(key);
}
public void put(int key, int value) {
if (cache.containsKey(key)){
// 修改key值
cache.put(key, value);
// 将key变为最近使用
makeRecently(key);
return;
}
if (cache.size() > this.cap){
Integer oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);
}
cache.put(key, value);
}
}
3 LinkedHashMap
LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。
除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。
在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。所以,要看懂 LinkedHashMap 的源码,需要先看懂 HashMap 的源码。
增加了一条双向链表,使得可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- 每当有新键值对节点插入,新节点最终会接在
tail引用指向的节点后面。而tail引用则会移动到新的节点上,这样一个双向链表就建立起来了。 - 上面的结构并不是很难理解,虽然引入了红黑树,导致结构看起来略为复杂了一些。但大家完全可以忽略红黑树,而只关注链表结构本身。
3.1 Entry 的继承体系
分析一下键值对节点的继承体系。
3.2 链表的建立过程
链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。
随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新。
Map 类型的集合类是通过 put(K,V) 方法插入键值对,LinkedHashMap 本身并没有覆写父类的 put 方法,而是直接使用了父类的实现。
// HashMap 中实现
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// HashMap 中实现
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) {...}
// 通过节点 hash 定位节点所在的桶位置,并检测桶中是否包含节点引用
if ((p = tab[i = (n - 1) & hash]) == null) {...}
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) {...}
else {
// 遍历链表,并统计链表长度
for (int binCount = 0; ; ++binCount) {
// 未在单链表中找到要插入的节点,将新节点接在单链表的后面
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) {...}
break;
}
// 插入的节点已经存在于单链表中
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {...}
afterNodeAccess(e); // 回调方法,后续说明
return oldValue;
}
}
++modCount;
if (++size > threshold) {...}
afterNodeInsertion(evict); // 回调方法,后续说明
return null;
}
// HashMap 中实现
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
// LinkedHashMap 中覆写
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 将 Entry 接在双向链表的尾部
linkNodeLast(p);
return p;
}
// LinkedHashMap 中实现
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
// last 为 null,表明链表还未建立
if (last == null)
head = p;
else {
// 将新节点 p 接在链表尾部
p.before = last;
last.after = p;
}
}
LinkedHashMap 覆盖了 newNode 方法。在这个方法中,LinkedHashMap 创建了 Entry,并通过 linkNodeLast 方法将 Entry 接在双向链表的尾部,实现了双向链表的建立。
双向链表建立之后,我们就可以按照插入顺序去遍历 LinkedHashMap。
3.3 链表节点的删除过程
3.4 访问顺序的维护过程
3.5 实现缓存
内容待补充!!!