请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
插入和查询使用哈希表即可以达到O(1)的时间复杂度,但是因为内存有限,我们要使用LRU算法更新缓存,所以我们一定要使用一个数据结构来记录”最近使用“这个数据,然后根据这个数据结构来选择键值对删除。
期望在 O(1)复杂度内调整某个节点在序列中的位置,很自然想到双向链表。
为了使用双向链表,我们考虑双向链表,结点我们自己创建的Node类,同时在哈希表内,我们使用Node类作为value,双向链表头部记录最近使用的结点,尾部记录最久没有使用的结点,并且在每次使用某个结点后将该结点移动到头部来,然后每次如果要溢出就清除尾部的最久没用的节点即可。
class LRUCache {
//链表节点,记录左右和键值对
class Node {
int key,value;
Node prev,succ;
Node(){
this.key=-1;
this.value=-1;
}
Node(int key,int value){
this.key=key;
this.value=value;
}
}
//容量初始化
int n;
//链表头尾节点
Node head, tail;
Map<Integer, Node> map;
//初始化
public LRUCache(int capacity) {
n=capacity;
map=new HashMap<>();
head=new Node();
tail=new Node();
head.succ=tail;
tail.prev=head;
}
public int get(int key) {
if(map.containsKey(key)){
Node node=map.get(key);
refresh(node);
return node.value;
}
return -1;
}
//放入数据,需要更新链表
public void put(int key, int value) {
Node node=null;
if(map.containsKey(key)){
node=map.get(key);
node.value=value;
}else{
if(map.size()==n){
Node toDel=tail.prev;
delete(toDel);
map.remove(toDel.key);
}
node=new Node(key,value);
map.put(key, node);
}
refresh(node);
}
// refresh 操作主要是将最新的节点移动到双链表头部
// 1. 先将当前节点从双向链表中删除(如果该节点本身存在于双向链表中的话)
// 2. 将当前节点添加到双向链表头部
void refresh(Node node) {
delete(node);
node.succ=head.succ;
node.prev=head;
head.succ.prev=node;
head.succ=node;
}
// delete 操作:将当前节点从双向链表中移除
// 由于我们预先建立 head 和 tail 两位哨兵,因此如果 node.l 不为空,则代表了 node 本身存在于双向链表(不是新节点)
void delete(Node node) {
if(node.succ!=null){
node.prev.succ=node.succ;
node.succ.prev=node.prev;
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
如果不要求O(1)的话,这题就使用哈希表就可以了,但是哈希表只满足插入和更新O(1)复杂度,想要找到最大/最小值复杂度就达到O(n)了,因此要考虑别的方法
class AllOne {
HashMap<String,Integer>map;
public AllOne() {
map=new HashMap<>();
}
public void inc(String key) {
map.put(key, map.getOrDefault(key, 0)+1);
}
public void dec(String key) {
int cnt=map.get(key);
if(cnt==1)map.remove(key);
else map.put(key,cnt-1);
}
public String getMaxKey() {
String ans="";
int max=0;
for(String s:map.keySet()){
if(map.get(s)>max){
max=map.get(s);
ans=s;
}
}
return ans;
}
public String getMinKey() {
String ans="";
int min=Integer.MAX_VALUE;
for(String s:map.keySet()){
if(map.get(s)<min){
min=map.get(s);
ans=s;
}
}
return ans;
}
}
也就是类似LRU的方法,区别是结点并不是键值对,而是根据出现次数来分类,并且将出现次数相同的字符串放在一起
class AllOne {
//节点记录所有出现次数相同的字符串,和他们的出现次数
class Node {
int cnt;
Set<String> set = new HashSet<>();
Node prev, succ;
Node(int cnt) {
this.cnt = cnt;
}
}
Node head, tail;
//map记录字符串对应节点
Map<String, Node> map;
//初始化
public AllOne() {
head = new Node(-1);
tail = new Node(-1);
map = new HashMap<>();
head.succ = tail;
tail.prev = head;
}
//如果一个节点已经没有数据,那么清空
//防止影响查找
void clear(Node node) {
if (node.set.size() == 0) {
node.prev.succ = node.succ;
node.succ.prev = node.prev;
}
}
public void inc(String key) {
//已有节点
if (map.containsKey(key)) {
Node node = map.get(key);
//增加也要将该字符从集合中移除,因为位置要调整了
node.set.remove(key);
int cnt = node.cnt;
//寻找cnt+1的位置
Node next = null;
//如果已有,直接插入
if (node.succ.cnt == cnt + 1) {
next = node.succ;
} else {
//如果没有,就要新建
next = new Node(cnt + 1);
next.succ = node.succ;
next.prev = node;
node.succ.prev = next;
node.succ = next;
}
//加入集合
next.set.add(key);
//加入哈希表
map.put(key, next);
//如果该节点只有这一个字符串,直接删除即可
clear(node);
} else {
//没有还要新建节点
Node node = null;
//寻找合适的插入位置,记住新节点的cnt==1
if (head.succ.cnt == 1) {
//已有出现次数为1的
node = head.succ;
} else {
//新建点一定是在head之后
node = new Node(1);
node.succ = head.succ;
node.prev = head;
head.succ.prev = node;
head.succ = node;
}
//直接将字符串加入到集合里,并更新哈希表
node.set.add(key);
//注意如果两个字符串出现次数相同,那么映射到同一个节点
map.put(key, node);
}
}
public void dec(String key) {
Node node = map.get(key);
//必然从当前位置移除,找新家
node.set.remove(key);
int cnt = node.cnt;
//如果该字符串只出现一次,那么直接移除该键值对
if (cnt == 1) {
map.remove(key);
} else {
//否则要找新家
//理论上是前一个
Node prev = null;
//前一个出现次数正好是n-1,直接插到前面即可
if (node.prev.cnt == cnt - 1) {
prev = node.prev;
} else {
//如果前一个出现次数不是cnt-1,要新建一个节点插在前一个和这个之间
prev = new Node(cnt - 1);
prev.succ = node;
prev.prev = node.prev;
node.prev.succ = prev;
node.prev = prev;
}
//加入set集合
prev.set.add(key);
//加入哈希表
map.put(key, prev);
}
clear(node);
}
public String getMaxKey() {
//最后的出现次数最多
Node node = tail.prev;
for (String str : node.set) return str;
return "";
}
public String getMinKey() {
//最前面的出现次数最少
Node node = head.succ;
for (String str : node.set) return str;
return "";
}
}