题目
1.思路分析:
推荐LRU此题题解视频
LRU的入门题。
获取或者添加数据的时间复杂度为O(1),第一个想到的数据结构就是HashMap。但是HashMap不能很方便地知道哪些元素是最近访问过的,所以还要借助一个别的数据结构实现这一功能。
这时候考虑到链表的删除和插入的时间复杂度都为O(1),所以考虑使用链表。
并且为了删除得更加方便,使用双向链表去实现。(双向链表比单向链表在这里有两个优点,1.双向链表可以通过该需要删除节点的引用,就能将该节点从链表中删除;而单项链表需要获取需要删除的前一个节点才能删除该节点。2.当双向链表头尾都有一个虚拟节点时,他就可以O(1)的时间复杂在头尾获取、删除、添加节点;相比较,单向链表如果只有一个虚拟头结点,他只能以O(1)的时间复杂度在头部获取、删除、添加节点)。
LRU(Least Recently Used)
LRU算法的思路分析是指对LRU(最近最少使用)算法的原理和实现方法进行解释和说明。LRU算法是一种缓存淘汰策略,它认为最近使用过的数据应该是有用的,而很久都没用过的数据应该是无用的,所以当缓存满了时,优先删除最久未使用的数据。
LRU算法的核心数据结构是哈希链表,也就是双向链表和哈希表的结合体。哈希表可以快速查找某个键是否存在缓存中,双向链表可以维护数据的访问顺序,并且可以快速删除、添加节点。
LRU算法的基本操作有两个:get (key) 和 put (key, value)。get (key) 用于获取键对应的值,如果键不存在则返回 -1。put (key, value) 用于存入或更新键值对,如果键已存在,则更新其值,并将其移动到链表尾部;如果键不存在,则创建一个新节点,并插入到链表尾部,同时检查缓存容量是否超出限制,如果超出则删除链表头部的节点。
LRU算法的时间复杂度为 O (1),因为哈希表和双向链表的操作都是常数时间的。LRU算法的空间复杂度为 O (capacity),因为需要存储 capacity 个键值对。
本题思路
创建一个具有双向链表的节点类Node,
class Node{
int key;
int value;
Node prev;
Node next;
public Node(int value){
this.value = value;
}
}
创建一个双向链表类DoubleLinkList,实现头尾虚拟节点,并且初始化他们的相互连接。
并且实现删除头结点,插入节点,插入尾节点功能。
再借助DoubleLinkList实现题目中的LRUCache类。
2. 算法实现
// 定义一个公共类LRUCache,表示一个最近最少使用缓存
public class LRUCache {
// 定义一个整型变量cap,表示缓存的容量
int cap = 0;
// 定义一个HashMap对象map,用于存储键值对
HashMap<Integer,Node> map;
// 定义一个DoubleLinkList对象link,用于维护缓存的顺序
DoubleLinkList link;
// 定义一个构造方法,接受一个整型参数capacity,初始化缓存的容量
public LRUCache(int capacity) {
cap = capacity;
map = new HashMap<>();
link = new DoubleLinkList();
}
// 定义一个公共方法get,接受一个整型参数key,返回对应的值
public int get(int key) {
// 如果map中不存在key,返回-1
if(map.get(key) == null){
return -1;
}
// 否则,获取map中key对应的节点的值,并赋值给val
int val = map.get(key).value;
// 调用put方法,将key和val作为参数,更新缓存
put(key,val);
// 返回val
return val;
}
// 定义一个公共方法put,接受两个整型参数key和value,将键值对添加或更新到缓存中
public void put(int key, int value){
// 创建一个Node对象node,传入key和value作为参数
Node node = new Node(key,value);
// 如果map中不存在key
if(map.get(key) == null){
// 如果map的大小已经达到或超过缓存的容量
if(map.size() >= cap) {
// 调用link的deleteHead方法,删除头部节点,并返回其key,赋值给k
int k = link.deleteHead();
// 从map中移除k对应的键值对
map.remove(k);
}
// 调用link的addTail方法,将node作为参数,添加到尾部
link.addTail(node);
// 将key和node作为键值对添加到map中
map.put(key,node);
}else{ // 否则,map中已经存在key
// 调用link的delete方法,将map中key对应的节点作为参数,删除该节点
link.delete(map.get(key));
// 调用link的addTail方法,将node作为参数,添加到尾部
link.addTail(node);
// 将key和node作为键值对更新到map中
map.put(key,node);
}
}
}
// 定义一个类Node,表示一个双向链表的节点
class Node{
// 定义一个公共整型变量key,表示节点的键
public int key;
// 定义一个公共整型变量value,表示节点的值
public int value;
// 定义一个公共Node变量prev,表示节点的前驱节点
public Node prev;
// 定义一个公共Node变量next,表示节点的后继节点
public Node next;
// 定义一个构造方法,接受两个整型参数key和value,初始化节点的键和值
public Node(int key,int value){
this.key = key;
this.value = value;
}
}
// 定义一个类DoubleLinkList,表示一个双向链表
class DoubleLinkList{
// 定义一个Node变量head,表示链表的头部哨兵节点
Node head;
// 定义一个Node变量tail,表示链表的尾部哨兵节点
Node tail;
// 定义一个构造方法,初始化
// 定义一个构造方法,初始化头部和尾部哨兵节点,并将它们相连
public DoubleLinkList(){
head = new Node(-1,1);
tail = new Node(-1,1);
head.next = tail;
tail.prev = head;
}
// 定义一个方法addTail,接受一个Node参数newNode,表示要添加到尾部的节点
public void addTail(Node newNode){
// 将newNode的next指向tail
newNode.next = tail;
// 将newNode的prev指向tail的prev
newNode.prev = tail.prev;
// 将tail的prev指向newNode
tail.prev = newNode;
// 将newNode的prev的next指向newNode
newNode.prev.next = newNode;
}
// 定义一个方法delete,接受一个Node参数node,表示要删除的节点,返回其key
public int delete(Node node){
// 将node的prev的next指向node的next
node.prev.next = node.next;
// 将node的next的prev指向node的prev
node.next.prev = node.prev;
// 返回node的key
return node.key;
}
// 定义一个方法deleteHead,不接受参数,表示删除头部节点,返回其key
public int deleteHead(){
// 调用delete方法,将head的next作为参数,返回其key
return delete(head.next);
}
}
3. 算法坑点
- 在DoubleLinkList类中,最后返回的应该是node.key,而不是node.value。