LeetCode原题
146. LRU 缓存【中等】,链接: link
解题思路
- 思路:采用哈希表(查找快,但是数据无固定顺序)+双向链表(有顺序之分,插入删除快,但是查找慢)组合,保证操作的时间复杂度 O(1)。
- 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
- 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
代码
Java
class LRUCache {
//思路1:利用JDK自带的LinkedHashMap
// LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<Integer, Integer>();
// int capacity;
// public LRUCache(int capacity) {
// this.capacity = capacity;
// }
// public int get(int key) {
// //调用时,更新该key的缓存顺序
// if (!cache.containsKey(key)) {
// return -1;
// }
// makeRecently(key);
// return cache.get(key);
// }
// public void put(int key, int value) {
// //若已存在
// if (cache.containsKey(key)) {
// cache.put(key, value);
// makeRecently(key);
// return;
// }
// //若不存在,判断是否触发淘汰策略
// if (cache.size() >= this.capacity) {
// int oldestKey = cache.keySet().iterator().next();
// cache.remove(oldestKey);
// }
// cache.put(key, value);
// }
// public void makeRecently(int key) {
// int value = cache.get(key);
// cache.remove(key);
// cache.put(key, value);
// }
//思路2:哈希表+手写实现双向链表
//3.实现LRUCache缓存类,包括封装API
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;
}
//将某个key提升为最近使用的
Node x = map.get(key);
cache.remove(x);
cache.addLast(x);
return x.val;
}
public void put(int key, int val) {
Node newNode = new Node(key, val);
if (map.containsKey(key)) {
// 先删除原节点
Node oldNode = map.get(key);
cache.remove(oldNode);
map.remove(key);
// 再添加最新节点
cache.addLast(newNode);
map.put(key, newNode);
return;
}
if (cache.size() >= this.capacity) {
// 删除最久未使用的节点
Node Oldest = cache.removeFirst();
map.remove(Oldest.key);
}
cache.addLast(newNode);
map.put(key, newNode);
}
//1.构建双向链表的节点类
class Node {
public int key;
public int val;
public Node prev;
public Node next;
public Node(int k, int v) {
this.key = k;
this.val = v;
}
}
// 2.构建双向链表的API
class DoubleList {
// 头尾虚节点
private Node head;
private Node tail;
// 链表元素数量
private int size;
public DoubleList() {
head = new Node(0,0);
tail = new Node(0,0);
head.next = tail;
tail.prev = head;
size = 0;
}
//在链表尾部添加节点x,时间O(1)
public void addLast(Node x) {
x.prev = tail.prev;
x.next = tail;
tail.prev.next = x;
tail.prev = x;
size++;
}
//删除链表中的x节点(x一定存在),时间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 == tail) {
return null;
}
Node first = head.next;
remove(first);
return first;
}
// 返回链表长度,时间O(1)
public int size() {
return size;
}
}
}
注意要点
- 细节
- 在面试中,面试官一般会期望面试者能够自己实现一个简单的双向链表,而不是使用语言自带的、封装好的数据结构。
- 为什么必须要用双向链表,而不是单链表?因为删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。
- 由于我们要同时维护一个双链表 cache 和一个哈希表 map,很容易漏掉一些操作,比如说删除某个 key 时,在 cache 中删除了对应的 Node,但是却忘记在 map 中删除 key。
- 延伸
- LRU(Least Recently Used)算法是按照访问顺序来淘汰缓存,也有按照访问频率进行淘汰缓存的LFU算法。
推荐相关
- 460 LFU 缓存
- 380 O(1) 时间插入、删除和获取随机元素