}
prev.next = new Node(e, prev.next);
size ++;
}
// 在链表尾部添加节点 x
public void addLast(Node x) {
add(size, x.val);
}
// 在链表头部添加节点 x
public void addFirst(Node x) {
add(0, x.val);
}
提供删除元素的方法:
- 根据索引删除
// 从链表中删除index处的元素
public Node remove(int index){
if (index < 0 || index > size)
throw new IllegalArgumentException(“Set failed. Illegal index.”);
Node prev = head;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node retNode = prev.next;
prev.next = retNode.next;
retNode.next = null;
size --;
return retNode;
}
- 删除传入节点
// 删除节点x
public void remove(Node x){
Node pre = head;
Node node = head.next;
while (node != null){
if (node == x){
pre.next = node.next;
node.next = null;
}
node = node.next;
pre = pre.next;
}
}
- 删除第一或最后一个元素
/**
-
删除最后一个节点
-
@return
*/
public Node removeLast() {
return remove(size - 1);
}
/**
-
删除第一个节点
-
@return
*/
public Node removeFirst(){
return remove(0);
}
提供根据数值寻找节点的方法:
/**
-
根据值寻找节点
-
@param val
-
@return
*/
public Node findByValue(int val){
Node node = head;
while (node != null){
if (node.val == val){
return node;
}
node = node.next;
}
return null;
}
LRU缓存算法类:
用链表的底层来实现缓存,构造函数传入初始容量
public class LRUCache {
private SingleList cache;
private int cap; // 最大容量
public LRUCache(int capacity) {
this.cap = capacity;
cache = new SingleList();
}
}
将某个val提升为最近使用的:
private void makeRecently(int val) {
Node x = cache.findByValue(val);
// 先从链表中删除这个节点
cache.remove(x);
// 重新插到队头
cache.addFirst(x);
}
添加最近使用的val:
private void addRecently(int val) {
Node x = new Node(val);
// 链表头部就是最近使用的元素
cache.addFirst(x);
}
删除某一个val:先找到对应节点,然后删除
private void deleteVal(int val) {
Node x = cache.findByValue(val);
// 从链表中删除
cache.remove(x);
}
删除最久未使用的val:
private void removeLeastRecently() {
// 链表尾部元素就是最久未使用的
cache.removeLast();
}
使用缓存中的某个数据:
public Node get(int val) {
Node x = cache.findByValue(val);
if (x == null){
return null;
}
// 将该数据提升为最近使用的
makeRecently(val);
return x;
}
放入新数据:如果缓存已经满了则需要删除最久未使用的
public void put(int val) {
if (cache.findByValue(val) != null) {
// 删除旧的数据
deleteVal(val);
// 新插入的数据为最近使用的数据
addRecently(val);
return;
}
if (cap == cache.size()) {
// 删除最久未使用的元素
removeLeastRecently();
}
// 添加为最近使用的元素
addRecently(val);
}
打印方法:
public void printCache(){
Node pointer = cache.head;
while (pointer != null){
System.out.print(“[” + pointer.val + “] ->”);
pointer = pointer.next;
}
System.out.println();
}
*3.1 优化
我们可以继续优化这个实现思路,比如引入散列表(Hash table)来记录每个数据的位置,将缓存访问的时间复杂度降到 O(1)。这里缓存改为存储键值对了,其实上面的单向链表的每个Node也可以存储两个值,key和value,有兴趣的同学们可以自己尝试一下
如果想让 put
和 get
方法的时间复杂度为 O(1),我们可以尝试总结以下几点:
-
cache
中的元素存储时必须能体现先后顺序,以区分最近使用的和久未使用的数据; -
要在
cache
中快速找某个key
是否存在并得到对应的val
; -
每次访问
cache
中的值,需要将其变为最近使用的,即,要支持在任意位置快速插入和删除元素。
哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。
所以,结合一下,用哈希链表
实际上LRU算法的核心数据结构就是哈希链表,长这样:
再看上面的条件:
-
同单链表的情况相同,如果从头部插入元素,显然越靠近尾部的元素是最久未使用的。(当然反过来也成立);
-
对于一个key,可以通过HashTable快速查找到val;
-
链表显然是支持在任意位置快速插入和删除的。只不过传统的链表查找元素的位置较慢,而这里借助哈希表,通过
key
快速映射到一个链表节点,然后进行插入和删除。
这次我们从尾部插入,从头部删除~
双向链表的节点类Node:
只增加了一个指向上一个节点的指针 Node pre
class Node {
public int key, val;
public Node next, prev;
public Node(int k, int v) {
this.key = k;
this.val = v;
}
}
依靠实现的节点类Node给出双向链表的实现:
构造方法初始化头尾虚节点,链表元素个数
class DoubleList {
// 头尾虚节点
public Node head, tail;
// 链表元素数
private int size;
public DoubleList() {
// 初始化双向链表的数据
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
size = 0;
}
}
在尾部添加节点的方法:
// 在链表尾部添加节点 x,时间 O(1)
public void addLast(Node x) {
x.prev = tail.prev;
x.next = tail;
tail.prev.next = x;
tail.prev = x;
size++;
}
删除节点x的方法:
删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,双向链表由于有pre指针的存在所以删除操作的时间复杂度为O(1)
// 删除链表中的 x 节点(x 一定存在)
// 由于是双链表且给的是目标 Node 节点,时间 O(1)
public void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
size–;
}
删除链表第一个节点的方法:
// 删除链表中第一个节点,并返回该节点,时间 O(1)
public Node removeFirst() {
if (head.next == tail)
return null;
Node first = head.next;
remove(first);
return first;
}
LRU缓存算法类:
有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可
构造方法:
class LRUCache {
// key -> Node(key, val)
private HashMap<Integer, Node> map;
// Node(k1, v1) <-> Node(k2, v2)…
private DoubleList cache;
// 最大容量
private int cap;
public LRUCache(int capacity) {
this.cap = capacity;
map = new HashMap<>();
cache = new DoubleList();
}
}
接下来先设计一些辅助函数,用于随后进行操作的:
将某个值设置为最近使用的:
private void makeRecently(int key) {
Node x = map.get(key);
// 先从链表中删除这个节点
cache.remove(x);
// 重新插到队尾
cache.addLast(x);
}
添加一个元素,该元素为最新使用的:
private void addRecently(int key, int val) {
Node x = new Node(key, val);
// 链表尾部是最近使用的元素
cache.addLast(x);
// 维护map
map.put(key, x);
}
删除元素:
private void deleteKey(int key) {
Node x = map.get(key);
// 从链表中删除
cache.remove(x);
// 维护map
map.remove(key);
}
**删除最久没用的元素:**其实就是删除第一个节点
private void removeLeastRecently() {
// 链表头部的第一个元素就是最久未使用的
Node deletedNode = cache.removeFirst();
// 维护map
int deletedKey = deletedNode.key;
map.remove(deletedKey);
}
有了上面这些辅助函数,接下来就可以实现LRU算法的get和put方法了:
get方法:
public int get(int key) {
if (!map.containsKey(key)) {
return -1;
}
// 将该数据提升为最近使用的
makeRecently(key);
return map.get(key).val;
}
put方法:
public void put(int key, int val) {
if (map.containsKey(key)) {
// 删除旧的数据
deleteKey(key);
// 新插入的数据为最近使用的数据
addRecently(key, val);
return;
}
if (cap == cache.size()) {
// 删除最久未使用的元素
removeLeastRecently();
}
// 添加为最近使用的元素
addRecently(key, val);
}
打印方法:
public void printCache(){
Node pointer = cache.head.next;
while (pointer != cache.tail){
System.out.print(“[” + pointer.key + “,” + pointer.val + “] ->”);
pointer = pointer.next;
}
System.out.println();
}
说了这么多其实也同时解决了 leetcode146 ,大家可以自行探索~
4 彩蛋
====
Java已经内置了相应的哈希链表LinkedHashMap这个数据结构我们可以直接拿过来用,逻辑和上文相同
class LRUCache {
int capacity; // 缓存大小
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
}
// 从缓存中取出数据方法
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
Java核心架构进阶知识点
面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的
内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
6241)]
[外链图片转存中…(img-Q01iQnKB-1713732366242)]
[外链图片转存中…(img-n6fLxlWY-1713732366243)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
Java核心架构进阶知识点
面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的
[外链图片转存中…(img-niMD3ZtE-1713732366243)]
[外链图片转存中…(img-ldwiIANx-1713732366244)]
[外链图片转存中…(img-M3Mdua8K-1713732366244)]
内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!