LRU缓存机制

来自Leetcode第146题LRU缓存机制

运用你所掌握的数据结构,设计和实现一个 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


硬件实现的4路组相联 + LRU算法

参考组原实验mooc

输入输出引脚图:

8个cache槽分2组4路:

替换算法LRU:

测试图:

软件实现

哈希链表实现

来源题解

LRU 算法实际上是让你设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。

注意哦,get 和 put 方法必须都是 O(1)O(1) 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。

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

LRUCache cache = new LRUCache(2);
// 你可以把 cache 理解成一个队列
// 假设左边是队头,右边是队尾
// 最近使用的排在队头,久未使用的排在队尾
// 圆括号表示键值对 (key, val)

cache.put(1, 1);
// cache = [(1, 1)]
cache.put(2, 2);
// cache = [(2, 2), (1, 1)]
cache.get(1); // 返回 1
// cache = [(1, 1), (2, 2)]
// 解释:因为最近访问了键 1,所以提前至队头
// 返回键 1 对应的值 1
cache.put(3, 3);
// cache = [(3, 3), (1, 1)]
// 解释:缓存容量已满,需要删除内容空出位置
// 优先删除久未使用的数据,也就是队尾的数据
// 然后把新的数据插入队头
cache.get(2); // 返回 -1 (未找到)
// cache = [(3, 3), (1, 1)]
// 解释:cache 中不存在键为 2 的数据
cache.put(1, 4);
// cache = [(1, 4), (3, 3)]
// 解释:键 1 已存在,把原始值 1 覆盖为 4
// 不要忘了也要将键值对提前到队头

从流程里可以看到,不需要设计计数器来实现对每次未命中的cache槽做计数+1,淘汰时将数值最大的淘汰。
在put时将元素放到了队头,每次get都会将元素提前,我们要做的就是把队尾元素替换掉。

LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:

代码如下:

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

class {
public int key,val;
public Node next,prior;

public (int key,int val){
this.val = val;
this.key = key;
}
}

class DoubleList{
private Node head,tail ; //头尾结点
private int size; //链表元素数

public DoubleList(){
head = new Node(0,0); //头尾虚结点
tail = new Node(0,0);
head.next = tail;< 大专栏  LRU缓存机制/span>
tail.prior = head;
size = 0;
}

//再头部添加结点
public void addFirst(Node x){
x.next = head.next;
x.prior = head;
head.next.prior = x;
head.next = x;
size++;
}

//删除链表中的结点
public void remove(Node x){
x.prior.next = x.next;
x.next.prior = x.prior;
size--;
}

// 删除链表中最后一个节点,并返回该节点
public Node removeLast(){
//当前链表没有结点
if(tail.prior == head)
return null;
Node last = tail.prior;
remove(last);
return last;
}

//返回链表长度
public int getSize(){
return size;
}
}

class LRUCache{
private HashMap<Integer ,Node> map;
private DoubleList cache; //双向链表
private int capacity;

public LRUCache(int capacity){
this.capacity = capacity;
map = new HashMap<>();
cache = new DoubleList();
}

public int get(int key){
if (!map.containsKey(key))
return -1;
int val = map.get(key).val;
//利用put方法把数据提前
put(key,val);
return val;
}

public void put(int key,int val){
// 先把新节点 x 做出来
Node x = new Node(key, val);

if (map.containsKey(key)) {
// 删除旧的节点,新的插到头部
cache.remove(map.get(key));
cache.addFirst(x);
// 更新 map 中对应的数据
map.put(key, x);
}else{
if (capacity == cache.getSize()) {
// 删除链表最后一个数据
Node last = cache.removeLast();
map.remove(last.key);
}
// 直接添加到头部
cache.addFirst(x);
map.put(key, x);
}
}
}

有序字典

来源题解

利用了LinkedHashMap这样一种数据结构
在构造器里第三个参数accessOrder,设置为false表示不是访问顺序而是插入顺序存储的,这也是默认值,表示LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的;true则是按照访问顺序,访问过后的元素会到链表的末尾。

0.75F表示负载因子
HashMap有一个初始容量大小,默认是16
static final int DEAFULT_INITIAL_CAPACITY = 1 << 4; // aka 16
为了减少冲突概率,当HashMap的数组长度达到一个临界值就会触发扩容,把所有元素rehash再放回容器中,这是一个非常耗时的操作。
而这个临界值由负载因子和当前的容量大小来决定:
DEFAULT_INITIAL_CAPACITYDEFAULT_LOAD_FACTOR
即默认情况下数组长度是16
0.75=12时,触发扩容操作。
所以使用hash容器时尽量预估自己的数据量来设置初始值。
在理想情况下,使用随机哈希吗,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素的个数和概率的对照表。
从上表可以看出当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为负载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
hash容器指定初始容量尽量为2的幂次方。
HashMap负载因子为0.75是空间和时间成本的一种折中。

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
class LRUCache extends LinkedHashMap<Integer, Integer>{
private int capacity;

public LRUCache(int capacity) {
super(capacity, 0.75F, true);
this.capacity = capacity;
}

public int get(int key) {
return super.getOrDefault(key, -1);
}

public void put(int key, int value) {
super.put(key, value);
}


protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}

/**
* LRUCache 对象会以如下语句构造和调用:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
原文地址:https://www.cnblogs.com/lijianming180/p/12409999.html