下面是2020年9月16日面试遇到的一道真实面试题。
题目
解析
LRU(Least Recently Used)最近最少使用。在O(1)时间复杂度完成获取和写入数据两个操作。
- 获取数据:定位到数据
- 写入数据:获取数据并且更新;或者如果仍有缓存直接写入;如果缓存不够需要清理原有的最近最少使用的数据后,写入新数据。
上面两个数据操作,O(1)时间复杂度查找到数据可以使用map数据结构,除此之外还有可能会清理原有的最近最少使用的数据,这样在数据保存的基础上还需要记录数据访问的先后顺序,这个顺序可以使用链表实现。那么,我们定义的数据结构就应该兼有map和list两种属性。map可以直接复用JDK中的HashMap,然后在这个基础上,让HashMap中节点具有双向链表的属性,链表中节点按照访问时间早晚的顺序排列(表头数据是最近访问的,表尾数据是最近最少使用的)。
在内存中数据存储应该如下图所示:
使用JDK中的HashMap,定义自己value的节点结构,增加具有双向链表的性质。关于双向链表可以参考Java的LinkedList源码解析。
如下定义LRUNode。
public class LRUNode{
int key;
int value;
LRUNode prev;
LRUNode next;
public LRUNode(){}
public LRUNode(int key, int value){
this.key = key;
this.value = value;
}
}
代码
两个操作逻辑:
- get操作:如果key不存在,返回-1;如果存在返回value,并且将这个key的LRUNode移动到链表的头部(moveToHead()函数)。
- put操作:如果key存在,更新value,将LRUNode移动链表的头部(moveToHead()函数);如果key不存在,判断当前缓存是否已满。如果缓存未满,直接map中放入新数据,并且新数据要连接到链表的头部(addToHead()函数);如果缓存已满,删除链表的尾部节点(最近最少使用的)(removeTailNode()函数),放入新数据并且连接到链表的头部(addToHead()函数)。
public class LRUCache {
//缓存容量
private int capacity;
//双向链表的头尾指针
private LRUNode head, tail;
//JDK的map作为存储
private Map<Integer, LRUNode> cache;
//缓存中当前实际的数据量
private int size;
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
cache = new HashMap<>();
//定义头指针
head = tail = new LRUNode();
}
//get操作
public int get(int key) {
LRUNode node = cache.get(key);
//如果key不存在,返回-1
if (null == node){
return -1;
}
//如果存在返回value,并且将这个key的LRUNode移动链表的头部
moveToHead(node);
return node.value;
}
//put操作
public void put(int key, int value) {
LRUNode node = cache.get(key);
//如果key存在
if (null != node){
//更新value
node.value = value;
//将LRUNode移动到链表的头部
moveToHead(node);
return;
}
//如果key不存在
//缓存未满
if (size < capacity){
size++;
} else {//缓存已满,删除链表的尾部节点
removeTailNode();
}
LRUNode newNode = new LRUNode(key, value);
//放入新数据
cache.put(key, newNode);
//新数据要连接到链表的头部
addToHead(newNode);
}
//删除链表的尾部节点
private void removeTailNode(){
if (head == tail){
return;
}
int key = tail.key;
tail = tail.prev;
tail.next = null;
cache.remove(key);
}
//新数据连接到链表的头部
private void addToHead(LRUNode node){
if (head == tail){
head.next = node;
node.prev = head;
tail = node;
return;
}
node.next = head.next;
head.next.prev = node;
head.next = node;
node.prev = head;
}
//移动节点到链表的头部
private void moveToHead(LRUNode node){
if (node.prev == head){
return;
}
LRUNode prev = node.prev;
if (node == tail){
prev.next = null;
tail = prev;
} else {
prev.next = node.next;
node.next.prev = prev;
}
node.next = head.next;
head.next.prev = node;
head.next = node;
node.prev = head;
}
}