动手写一个LRU缓存
前言
LRU 是 Least Recently Used 的简写,字面意思则是最近最少使用。
通常用于缓存的淘汰策略实现,由于缓存的内存非常宝贵,所以需要根据某种规则来剔除数据保证内存不被占满。
在redis的数据淘汰策略中就包含LRU淘汰算法
如何实现一个完整的LRU缓存呢?这个缓存要满足:
- 这个缓存要记录使用的顺序
- 随着缓存的使用变化,要能更新缓存的顺序
基于这种特点,可以使用一个常用的数据结构:链表
- 每次加入新的缓存时都添加到链表的
头节点 - 当缓存再次被使用时移动缓存到
头节点 - 当添加的缓存超过能够缓存的最大时,删除链表
尾节点的元素
单链表和双向链表的选择
单链表的方向只有一个,节点中有一个next指针指向后继节点。而双链表有两个指针,可以支持两个方向,每个节点不止有一个next指针,还有一个prev指针指向前驱节点
双向链表需要额外的两个空间来存放前驱节点的指针prev和后继节点指针next,所以,存储相同大小的数据,双向链表需要更多的空间。虽然相比单向链表,双向链表的每个节点多个一个指针空间,但是这样的结构带来了更多的灵活性,在某些场景下非常适合使用这样的数据结构。删除和添加节点操作,双向链表的时间复杂度为O(1)
在单向链表中,删除和添加节点的时间复杂度已经是O(1)了,双向链表还能比单向链表更加高效吗?
先来看看删除操作:
在删除操作中有两种情况:
- 删除给定值的节点
- 删除给定指针的节点
对于第一种情况,无论是删除给定值或者是给定的指针都需要从链表头开始依此遍历,直到找到所要删除的值
尽管删除这个操作的时间复杂度为O(1),但是删除的时间消耗主要是遍历节点,对应的时间复杂度为O(n),所以总的时间复杂度为O(n)。
对于第二种情况,已经给定了要删除的节点,如果使用单向链表,还得从链表头部开始遍历,直到找到待删除节点的前驱节点。但是对于双向链表来所,这就很有优势了,双向链表的待删除节点种包含了前驱节点,删除操作只需要O(1)的时间复杂度
同理对于添加操作:
我们如果想要在指定的节点前面或者后面插入一个元素,双向了链表就有很大的优势,他可以在O(1)的时间复杂度搞定,而单向链表还需要从头遍历。
所以,虽然双向链表比单向链表需要更多的存储空间,但是双向链表的应用更加广泛,JDK种LinkedHashMap这种数据结构就使用了双向链表
如何实现LRU缓存
单链表实现
下面我们基于单链表给出简单的代码实现:
package com.ranger.lru;
import java.util.HashMap;
import java.util.Map;
/**
*
* @author ranger
* LRU缓存
*
*/
public class LRUMap<K,V> {
/**
* 定义链表节点
* @author ranger
*
* @param <K>
* @param <V>
*/
private class Node<K, V> {
private K key;
private V value;
Node<K, V> next;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
public Node() {
}
}
/**
* 缓存最大值
*/
private int capacity;
/**
* 当前缓存数量
*/
private int size;
/**
* 缓存链表头节点
*/
private Node<K,V> head;
/**
* 缓存链表尾节点
*/
private Node<K,V> tail;
/**
* 定义带参构造函数,构造一个为空的双向链表
* @param capacity 缓存最大容量
*/
public LRUMap(int capacity) {
this.capacity = capacity;
head = null;
tail = null;
size = 0;
}
/**
* 无参构造函数,初始化容量为16
*/
public LRUMap() {
this(16);
}
/**
* 向双向链表中添加节点
* @param key
* @param value
*/
public void put(K key,V value) {
addNode(key,value);
}
/**
* 根据key获取缓存中的Value
* @param key
* @return
*/
public V get(K key) {
Node<K,V> retNode = getNode(key);
if(retNode != null) {
// 存在,插入头部
moveToHead(retNode);
return retNode.value;
}
// 不存在
return null;
}
/**
* 移动给定的节点到头节点
* @param node
*/
public void moveToHead(Node<K,V> node) {
// 如果待移动节点是最后一个节点
if(node == tail) {
Node prev = head;
while(prev.next != null && prev.next != node) {
prev = prev.next;
}
tail = prev;
node.next = head;
head = node;
prev.next = null;
}else if(node == head){ // 如果是头节点
return;
}else {
Node prev = head;
while(prev.next != null && prev.next != node) {
prev = prev.next;
}
prev.next = node.next;
node.next = head;
head = node;
}
}
/**
* 获取给定key的节点
* @param key
* @return
*/
private Node<K,V> getNode(K key){
if(isEmpty()) {
throw new IllegalArgumentException("list is empty,cannot get node from it");
}
Node<K,V> cur = head;
while(cur != null) {
if(cur.key.equals(key)) {
return cur;
}
cur = cur.next;
}
return null;
}
/**
* 添加到头节点
* @param key
* @param value
*/
private void addNode(K key,V value) {
Node<K,V> node = new Node<>(key,value);
// 如果容量满了,删除最后一个节点
if(size == capacity) {
delTail();
}
addHead(node);
}
/**
* 删除最后一个节点
*/
private void delTail() {
if(isEmpty()) {
throw new IllegalArgumentException("list is empty,cannot del from it");
}
// 只有一个元素
if(tail == head) {
tail = null;
head = tail;
}else {
Node<K,V> prev = head;
while(prev.next != null && prev.next != tail) {
prev = prev.next;
}
prev.next = null;
tail = prev;
}
size--;
}
/**
* 链表是否为空
* @return
*/
private boolean isEmpty() {
return size == 0;
}
/**
* 添加节点到头头部
* @param node
*/
private void addHead(Node node) {
// 如果链表为空
if(head == null) {
head = node;
tail = head;
}else {
node.next = head;
head = node;
}
size ++;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Node<K,V> cur = head;
while(cur != null) {
sb.append(cur.key)
.append(":")
.append(cur.value);
if(cur.next != null) {
sb.append("->");
}
cur = cur.next;
}
return sb.toString();
}
/**
* 测试
* @param args
*/
public static void main(String[] args) {
LRUMap<String,String> lruMap = new LRUMap(3) ;
lruMap.put("1","tom") ;
lruMap.put("2","lisa") ;
lruMap.put("3","john") ;
System.out.println(lruMap.toString());
lruMap.put("4","july") ;
System.out.println(lruMap.toString());
lruMap.put("5","jack") ;
System.out.println(lruMap.toString());
String value = lruMap.get("3");
System.out.println(lruMap.toString());
System.out.println("the value is: "+value);
String value1 = lruMap.get("1");
System.out.println(value1);
System.out.println(lruMap.toString());
}
}
输出结果:
3:john->2:lisa->1:tom
4:july->3:john->2:lisa
5:jack->4:july->3:john
3:john->5:jack->4:july
the value is: john
null
3:john->5:jack->4:july
LinkedHashMap实现
了解LinkedHashMap的都知道,它是基于链表实现,其中还有一个 accessOrder 成员变量,默认是 false,默认按照插入顺序排序,为 true 时按照访问顺序排序,也可以调用 构造函数传入accessOrder
LinkedHashMap 的排序方式有两种:
- 根据写入顺序排序。
- 根据访问顺序排序。
其中根据访问顺序排序时,每次 get 都会将访问的值移动到链表末尾,这样重复操作就能的到一个按照访问顺序排序的链表
我们可以重写LinkedHashMap中的removeEldestEntry方法来决定在添加节点的时候是否需要删除最久未使用的节点
代码实现如下:
public class LRULinkedHashMap<K,V> {
/**
* 缓存map
*/
private LinkedHashMap<K,V> cacheMap;
/**
* 当前缓存数量
*/
private int size;
/**
* 构造一个cacheMap,并设置可以缓存的数量
* @param size
*/
public LRULinkedHashMap(int size) {
this.size = size;
cacheMap = new LinkedHashMap<K,V>(16,0.75F,true) {
@Override
// 重写方法,判断是否删除最久没使用的节点
protected boolean removeEldestEntry(Map.Entry eldest) {
if (size + 1 == cacheMap.size()){
return true ;
}else {
return false ;
}
}
};
}
/**
* 添加缓存
* @param key
* @param value
*/
public void put(K key,V value){
cacheMap.put(key,value) ;
}
/**
* 获取缓存
* @param key
* @return
*/
public V get(K key){
return cacheMap.get(key) ;
}
public String toString() {
StringBuilder sb = new StringBuilder();
Set<Entry<K, V>> entrySet = cacheMap.entrySet();
for (Entry<K,V> entry : entrySet) {
sb.append(entry.getKey())
.append(":")
.append(entry.getValue())
.append("<-");
}
return sb.toString();
}
public static void main(String[] args) {
LRULinkedHashMap<String,Integer> map = new LRULinkedHashMap(3) ;
map.put("1",1);
map.put("2",2);
map.put("3",3);
System.out.println(map);
map.put("4", 4);
System.out.println(map);
}
}
动手写一个LRU缓存的更多相关文章
- 动手实现一个 LRU cache
前言 LRU 是 Least Recently Used 的简写,字面意思则是最近最少使用. 通常用于缓存的淘汰策略实现,由于缓存的内存非常宝贵,所以需要根据某种规则来剔除数据保证内存不被撑满. 如常 ...
- 搞定redis面试--Redis的过期策略?手写一个LRU?
1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...
- 动手写一个简单版的谷歌TPU-指令集
系列目录 谷歌TPU概述和简化 基本单元-矩阵乘法阵列 基本单元-归一化和池化(待发布) TPU中的指令集 SimpleTPU实例: (计划中) 拓展 TPU的边界(规划中) 重新审视深度神经网络中的 ...
- 【redis前传】自己手写一个LRU策略 | redis淘汰策略
title: 自己手写一个LRU策略 date: 2021-06-18 12:00:30 tags: - [redis] - [lru] categories: - [redis] permalink ...
- 动手写一个简单版的谷歌TPU-矩阵乘法和卷积
谷歌TPU是一个设计良好的矩阵计算加速单元,可以很好的加速神经网络的计算.本系列文章将利用公开的TPU V1相关资料,对其进行一定的简化.推测和修改,来实际编写一个简单版本的谷歌TPU.计划实现到行为 ...
- 死磕 java同步系列之自己动手写一个锁Lock
问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...
- 死磕 java线程系列之自己动手写一个线程池
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...
- 自己动手写一个服务网关-java
自己动手写一个服务网关 原文链接:https://www.cnblogs.com/bigben0123/p/9252444.html 引言 什么是网关?为什么需要使用网关? 如图所示,在不使用网关的情 ...
- 动手写一个简单的Web框架(模板渲染)
动手写一个简单的Web框架(模板渲染) 在百度上搜索jinja2,显示的大部分内容都是jinja2的渲染语法,这个不是Web框架需要做的事,最终,居然在Werkzeug的官方文档里找到模板渲染的代码. ...
随机推荐
- CentOS7中安装pip的方法
1.安装epel-release [root@localhost ~]# yum -y install epel-release 2.安装python-pip [root@localhost ~]# ...
- JDBC 处理sql查询多个不确定参数
JDBC程序,为了防止SQL注入,通常需要进行参数化查询,但是如果存在多个不确定参数,就比较麻烦了,查阅了一些资料,最后解决了这个问题,现在这里记录一下: public List<TabDl ...
- 聊聊docker那些端口问题
今天来系统聊一聊docker的端口,常见的有容器内程序端口.容器端口.主机端口.Dockerfile中EXPOSE端口.docker-compose和docker run中的port等. 貌似很多端口 ...
- 使用Crossplane构建专属PaaS体验:Kubernetes,OAM和CoreWorkflows
关键点:•许多组织的目标是构建自己的云平台,这些平台通常由内部部署架构和云供应商共建完成.•虽然Kubernetes没有提供开箱即用的完整PaaS平台式服务,但其具备完整的API,清晰的技术抽取和易理 ...
- 单片机main函数退出后发生什么——以stm32为例
STM32:main函数退出后发生什么? 我们都在说单片机要运行在无限循环里,不能退出,可退出之后会发生什么? 讨论STM32启动过程的文章数不胜数,可main函数结束之后会发生什么却少有讨论. 几日 ...
- 搭建服务器之FTP
FTP服务器,使用软件vsftpd,服务守护进程也是vsftpd.客户端访问的话可以用浏览器或ftp命令行. 1.yum install vsftpd.安装简单主要是配置,这个比httpd复杂点的地方 ...
- vue学习13-自定义组件
1 <!DOCTYPE html> 2 <html lang='en'> 3 <head> 4 <meta charset='UTF-8'> 5 < ...
- golang中数组指针和指针数组当做函数参数如何修改数组中的值
先理解:数组指针它的类型时指针,指针数组它的类型时数组 1. 数组指针当做函数的参数 package main import "fmt" func changeData(dataA ...
- Go 命令行参数,JSON 序列化与反序列化
#### Go 命令行参数,JSON 序列,反序列化这一节来学习一下Go 如果解析命令行参数,以及JSON 的序列化及反序列化; 命令行参数对于熟悉Linux 的同学来说比较清楚,如: ls -a , ...
- 【数据结构】K-D Tree
K-D Tree 这东西是我入坑 ICPC 不久就听说过的数据结构,但是一直没去学 QAQ,终于在昨天去学了它.还是挺好理解的,而且也有用武之地. 目录 简介 建树过程 性质 操作 例题 简介 K-D ...