所谓LRU算法,全称是Least Recently Used,其主要的思想就是:
如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。所以,当指定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
在设计数据结构的时候,基于效率的角度看,要满足算法put()、get()操作的时间复杂度都为O(1),同时为了区分最近使用和最久使用,还要保持数据的顺序。所以这个数据结构应该满足这几个特点:查找快、插入快、删除快、有顺序。
在常用的几种数据结构中,哈希表是查找、插入、删除快,但是无序;双向链表是保持了有序性,但是操作很慢。因此,将两者结合起来,就组成了一个新的数据结构:哈希链表。
针对这种数据结构,我们来分析一下LRU的整个流程
- 1.如果每次默认从链表头部添加元素,那么显然越靠近头部的元素就越是最近使用的。越靠近尾部的元素就是越久未使用的。
- 2.对于某一个 key ,可以通过哈希表快速定位到链表中的节点,从而取得对应的 value。
- 3.链表显然是支持在任意位置快速插入和删除的,修改指针就行。但是链表无法按照索引快速访问某一个位置的元素,都是需要遍历链表的,所以这里借助哈希表,可以通过 key,快速的映射到任意一个链表节点,然后进行插入和删除。
代码实现:
双链表的节点类:
class Node {
public int key, val;
public Node next, prev;
public Node(int k, int v) {
this.key = k;
this.val = v;
}
}
然后依靠我们的 Node 类型构建一个双链表DoubleList,实现几个要用到的 API,这些操作的时间复杂度均为 O(1) :
class DoubleList {
// 在链表头部添加节点 x
public void addFirst(Node x){...};
// 删除链表中的 x 节点(x 一定存在)
public void remove(Node x){...};
// 删除链表中最后一个节点,并返回该节点
public Node removeLast(){...};
// 返回链表长度
public int size(){...};
}
private HashMap<INteger, Node> map;
private DoubleList cache;
private int cap; //最大容量
public LRUCache(int capacity) {
this.cap = 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) {
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(cap == cache.size()) {
// 删除链表最后一个数据
Node last = cache.removeLast():
mpa.remove(last.key);
}
// 直接添加到头部
cache.addFirst(x);
map.put(key, x);
}
}
}
最后,解决几个疑问:
1、为什么要用双向链表?
因为涉及到删除元素的操作,且要求时间复杂度为O(1),所以需要指向前驱节点的指针。
2、哈希表中已经存了key,链表中为什么还要存key和value?
因为我们在删除链表最后一个节点后,还需要删除对应哈希表中的key。
if(cap == cache.size()) {
// 删除链表最后一个数据
Node last = cache.removeLast():
mpa.remove(last.key);
}