点击查看原题——146. LRU缓存机制
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
进阶:
你是否可以在 O(1) 时间复杂度内完成这两种操作?
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
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)]
cache.put(3, 3); // 该操作会使得密钥 2 作废
//cache[(3,3),(1,1)]
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
//cache [(4,4),(3,3)]
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
// cache[(3,3),(4,4)]
cache.get(4); // 返回 4
//cache[(4,4),(3,3)]
分析
LRU算法实际上是:最近操作的数据位于数据结构的队首。并且put和get操作的时间复杂度均是O(1)。
实际上这个Cache的特点:查找快、删除快、插入快、有顺序。
- HashMap除了无序,其他3个特点均满足;
- 链表是有序的,并且删除和插入的时间复杂度均为O(1)。虽然HashMap可以快速定位到Node节点,但单链表中只保存了一个指针,无法进行删除操作。所以使用双向链表,即使定位到一个节点,也可以确定其前节点和后节点。
故最终的存储结构是哈希表与双向链表的结合。
- 哈希表定位Node节点:解决链表的查找缓慢问题;
- 双向链表存储Value:解决哈希表的无序问题;
哈希表存储key,用于定位到链表汇总的value。那么Node中为什么还要存储key呢?
因为Node自我删除的时候,要去Map中删除对应的key。但若是Node只保存Value,没有保存key。找不到对应的Map数据。即Node节点也需要映射map。
代码实现
1. 创建Node节点。
public class LRUCache {
static class Node {
//链表数据域为key和val
private int key, val;
private Node next, prev;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
}
2. 创建双向链表,并实现其中的API方法
public class LRUCache {
static class DoubleList {
private Node head, tail; //头节点,尾节点
private int size;
//构建双向链表
public DoubleList() {
this.head = new Node(0, 0);
this.tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
this.size = 0;
}
//链表头部插入数据
public void addFirst(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
size++;
}
//删除链表中的node节点
public void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
size--;
}
//删除链表的最后一个节点,并返回该节点
public Node removeLast() {
if (tail.prev == head) {
return null;
}
//保存尾指针
Node lastNode = tail.prev;
tail.prev.prev.next = tail;
tail.prev = tail.prev.prev;
size--;
return lastNode;
}
//返回链表长度
public int size() {
return size;
}
}
}
3. 实现LRU缓存
public class LRUCache {
//记录 节点的位置
HashMap map = new HashMap<>(16);
//真正的缓存
DoubleList cache = new DoubleList();
//最大容量
private int cap;
public LRUCache(int capacity) {
cap = capacity;
}
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 value) {
//先new出新节点
Node node = new Node(key, value);
//若包含节点,则删除节点后,插入到头部
if (map.containsKey(key)) {
//删除Node节点
cache.remove(map.get(key));
cache.addFirst(node);
//更新map中的值
map.put(key, node);
} else {
//若没有包含头节点
if (cap == cache.size()) {
//删除链表的最后一个元素
Node last = cache.removeLast();
//注:若Node中值存储value,那么此处不能映射到map。
map.remove(last.key);
}
//插入链表头部
cache.addFirst(node);
map.put(key, node);
}
}
}
实际上在Java中LinkedHashMap便是使用哈希表+双向链表实现的LRU思想。