目录
一、该题基础信息列表
二、【LRU缓存结构】编程练习目标
三、题目分析及解析思路
3-1、设计LRU缓存结构
3-2、解题思路 与 具体实现(JAVA篇)
(1) 结构体: 双向链表
(2)全局变量的设置和初始值
(3) LRU插入操作具体实现
(4) LRU查询操作具体实现
(5) 上面步骤中使用到的方法具体实现【难点】
(5-1) 移动到头节点moveToHead
(5-2) 节点插入到头节点后 insertToHead
(5-3) 删除尾节点removeLastTail()
四、代码完整实现(JAVA篇)
设计LRU缓存结构 | LeetCode | 牛客【题库--算法篇--面试高频榜单】 |
题序号 | 146. LRU 缓存 |
NC93 设计LRU缓存结构 |
难度 & 频次 | 【middle】 | 【hard】【高频】 |
这题据身边大厂朋友反馈,如字节,小米等公司,笔试频率非常高。非常经典,且有一定代码量,即能考察程序员的基础也能有一定的拔高,请务必熟练掌握。
本题考点:【链表】【哈希】
方法:哈希表+双向链表(推荐使用)
知识点1:哈希表
哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。
知识点2:双向链表
双向链表是一种特殊的链表,它除了链表具有的每个节点指向后一个节点的指针外,还拥有一个每个节点指向前一个节点的指针,因此它可以任意向前或者向后访问,每次更改节点连接状态的时候,需要变动两个指针。
思路:
题目要求,插入和查询必须要求 O(1),是本题的难点!因为任何一个数据结构都不能直接做到O(1), 因此只能组合使用。
插入O(1)的数据结构有很多,但在目标点插入,且超出长度要O(1)之内删除,可用链表。key值节点加入链表头,同时删掉链表尾,选择双向链表,便于删除与移动。
//双向链表结构体
class Node{
int key;
int val;
Node pre;
Node next;
//进行初始化
Node(int key, int val){
this.key = key;
this.val = val;
this.pre = null;
this.next = null;
}
}
记录双向链表的头、尾及LRU剩余的大小,并全部初始化,首尾相互连接好。
//全局变量
//哈希表用于O(1)查询
Map map = new HashMap<>();
//双向链表,head,tail指针
Node head = new Node(-1,-1);
Node tail = new Node(-1,-1);
//缓存剩余空间数
int k = 0;
//对全局变量进行初始赋值
public LRUCache(int capacity) {
head.next = tail;
tail.pre = head;
k = capacity;
}
存在两种可能性:
(3-1)已经存在(通过哈希表的map判断),此时获取已经存在的节点,重新赋val值, 并把该节点移动到链表头head
(3-2)之前不存在,需要插入,先判断此时缓存剩余空间知否足够,不充足则需要删除链表尾节点tail,插入到链表头head。
public void put(int key, int value){
//存在该节点
if(map.containsKey(key)){
map.get(key).val = value;
//该节点移动到链表头
moveToHead(map.get(key));
}else{
//不存在该节点,则创建,并写入哈希表中
Node node = new Node(key,value);
map.put(key,node);
//如果没有缓存空间,则删除最后一个元素
if(k <= 0){
removeLastTail();
}
//减少缓存剩余空间,并向头插入节点
k--;
insertToHead(node);
}
}
如果当前哈希表map中存在key, 则返回对应的value值,并把该节点移动到链表头head.
如果哈希表map不存在key, 则直接返回-1
public int get(int key) {
int res = -1;
//哈希表存在,则返回述职,并移动到链表头head
if(map.containsKey(key)){
Node node = map.get(key);
res = node.val;
moveToHead(node);
}
return res;
}
我们分析一下,上面步骤中使用到哪个方法,并逐一代码实现
不论是查询已有节点,还是写入新节点,都需要移动到链表头head
void moveToHead(Node node){
//已经到表头,直接返回
if(head.next == node)
return;
//删除原节点
node.pre.next = node.next;
node.next.pre = node.pre;
//节点插入到头节点后
insertToHead(node);
}
不存是写入新节点(3)的set方法,还是(5-1)的移动到头节点moveToHead, 都需要把节点插入到头节点
void insertToHead(Node node){
node.next = head.next;
node.pre = head;
head.next.pre = node;
head.next =node;
}
在缓存空间满的时候,如果需要新插入数据(3)的set方法中k<=0(缓存空间满), 删除缓存尾节点
void removeLastTail(){
//哈希表需要删除这个key
map.remove(tail.pre.key);
//断连该节点
tail.pre.pre.next = tail;
tail.pre = tail.pre.pre;
}
import java.util.*;
class Node{
int key;
int val;
Node pre;
Node next;
//初始化
Node(int key, int val){
this.key = key;
this.val = val;
this.pre = null;
this.next = null;
}
}
class LRUCache {
//全局变量,哈希表,头尾指针,剩余空间
Map map = new HashMap<>();
Node head = new Node(-1,-1);
Node tail = new Node(-1,-1);
int k = 0;
//给全局变量进行赋值
public LRUCache(int capacity) {
head.next = tail;
tail.pre = head;
k = capacity;
}
public int get(int key) {
int res = -1;
if(map.containsKey(key)){
Node node = map.get(key);
res = node.val;
moveToHead(node);
}
return res;
}
public void put(int key, int value) {
if(map.containsKey(key)){
//给这个已有节点赋新value
map.get(key).val = value;
moveToHead(map.get(key));
}else{
//插入新节点, 哈希表写入新节点
Node node = new Node(key,value);
map.put(key,node);
if(k <= 0){
//缓存空间已满,删除尾节点
removeLastTail();
}
//插入到头节点, 空间容量减少
k--;
insertToHead(node);
}
}
void moveToHead(Node node){
//此时node已经是头节点了
if(head.next == node)
return;
node.pre.next = node.next;
node.next.pre = node.pre;
insertToHead(node);
}
void insertToHead(Node node){
node.pre = head;
node.next = head.next;
head.next.pre = node;
head.next = node;
}
void removeLastTail(){
//哈希表删除尾节点
map.remove(tail.pre.key);
//链表删除尾
tail.pre.pre.next = tail;
tail.pre = tail.pre.pre;
}
}