LRU概述

LRU算法,即最近最少使用算法。其使用场景非常广泛,像我们日常用的手机的后台应用展示,软件的复制粘贴板等。
本文将基于算法思想手写一个具有LRU算法功能的Java工具类。

结构设计

在插入数据时,需要能快速判断是否已有相同数据。为实现该目的,可以使用hash表结构。
同时根据LRU的规则,在对已有元素进行查找和修改操作后,该元素应该被置于首位;在增加元素时,如果超过了最大容量,则会淘汰末尾元素。为减少元素移动的时间复杂度,这里采用双端链表结构,使得移动元素到首位和删除末尾元素的时间复杂度都为O(1)。
根据上述数据结构,可以定义元素节点内容,包含hash值,键K,值value,先继节点和后继节点。如下所示:
 1 static class Entry<K,V> {
2 final int hash; // 哈希值
3 final K key; // 键
4 V value; // 值
5 Entry<K,V> before; // 先继节点
6 Entry<K,V> after; // 后继节点
7 Entry(int hash, K key, V value, Entry before, Entry after) {
8 this.hash = hash;
9 this.key = key;
10 this.value = value;
11 this.before = before;
12 this.after = after;
13 }
14 }
双端链表则需要存储头节点和尾节点。
其它成员变量如下:
1 int maxSize;            // 最大容量
2 Entry<K,V> head; // 头节点
3 Entry<K,V> tail; // 尾节点
4 HashMap<K,V> hashMap; // 哈希表
在实现容器的增删改查方法前,我们先把一些对链表的共用操作抽象出来,包括查找链表节点、将链表节点移动到队首、删除链表中节点。对应方法实现如下:
 1 // 根据key从链表中找对应节点
2 Entry<K, V> find(Object key) {
3 // 遍历链表找到该元素
4 Entry<K,V> entry = head;
5 while (entry != null) {
6 if (entry.key.equals(key))
7 break;
8 entry = entry.after;
9 }
10 return entry;
11 }
12 // 将key对应的元素移至队首
13 Entry<K,V> moveToFront(Object key) {
14 // 遍历链表找到该元素
15 Entry<K,V> entry = find(key);
16 // 如果找到了并且不是队首,则将该节点移动到队列的首部
17 if (entry != null && entry != head) {
18 // 如果该节点是队尾
19 if (entry == tail)
20 tail = entry.before;
21 // 先将该节点从链表中移出
22 Entry<K,V> p = entry.before;
23 Entry<K,V> q = entry.after;
24 p.after = q;
25 if (q != null)
26 q.before = p;
27 // 然后将该节点作为新的head
28 entry.before = null;
29 entry.after = head;
30 head = entry;
31 }
32 return entry;
33 }
34 // 将key对应的元素从双端链表中删除
35 void removeFromLinkedList(Object key) {
36 // 遍历链表找到该元素
37 Entry<K,V> entry = find(key);
38 // 如果没找到则直接返回
39 if (entry == null) return;
40 // 如果是队首元素
41 if (entry == head) {
42 // 只有一个节点
43 if (tail == head)
44 tail = entry.after;
45 head = entry.after;
46 head.before = null;
47 } else if (entry == tail) {
48 // 如果是队尾元素
49 tail = tail.before;
50 tail.after = null;
51 }
52 }

put()方法

put元素时需要判断元素是否已经在容器中存在,如果存在,则修改对应节点的值,并将该节点移动到链表的头部。
如果不存在,则将元素插入到链表的头部。如果此时容量超过预设最大容量,需要将队列尾部元素移除。
注意:上述操作需要判断是否更新头尾节点。
代码如下:
 1 // 存入元素/修改元素
2 public void put(K key, V value) {
3 V res = hashMap.put(key,value);
4 // 如果res为null,表示没找到,则存入并放置到队首
5 if (res == null) {
6 Entry<K,V> entry = new Entry<>(key.hashCode(), key, value, null, head);
7 // 如果之前没有头节点
8 if (head == null) {
9 head = entry;
10 tail = entry;
11 } else {
12 // 如果之前有头节点,将头节点before指向entry
13 entry.after = head;
14 head.before = entry;
15 head = entry;
16 }
17 // 判断此时节点数量是否超过最大容量,如果超过,则将队尾元素删除
18 if (hashMap.size() > maxSize) {
19 tail = tail.before;
20 tail.after = null;
21 }
22 } else {
23 // 如果res不为null,表示包含该元素,则将节点放置到队首
24 Entry<K,V> entry = moveToFront(key);
25 // 同时修改节点的V值
26 entry.value = value;
27 }
28 }

remove()方法

从容器中删除元素,需要判断是否在容器中存在。同时也要注意更新头尾节点。
1 // 删除元素
2 public void remove(Object key) {
3 V res = hashMap.remove(key);
4 // 如果删除成功,则将链表中节点一并删除
5 if (res != null)
6 removeFromLinkedList(key);
7 }

get()方法

查找元素如果找到的话需要将对应节点移动到队列头部。
1 // 查询元素
2 public V get(Object key) {
3 V res = hashMap.get(key);
4 // 如果在已有数据中找到,则将该元素放置到队首
5 if (res != null)
6 moveToFront(key);
7 return res;
8 }

完整代码

完整代码以及测试如下:
  1 package com.simple.test;
2
3 import java.util.ArrayList;
4 import java.util.HashMap;
5 import java.util.List;
6
7 public class SimpleLRUCache <K,V>{
8 int maxSize; // 最大容量
9 Entry<K,V> head; // 头节点
10 Entry<K,V> tail; // 尾节点
11 HashMap<K,V> hashMap; // 哈希表
12 // 构造函数
13 public SimpleLRUCache(int size) {
14 if (size <= 0)
15 throw new RuntimeException("容器大小不能<=0");
16 this.maxSize = size;
17 this.hashMap = new HashMap<>();
18 }
19 static class Entry<K,V> {
20 final int hash; // 哈希值
21 final K key; // 键
22 V value; // 值
23 Entry<K,V> before; // 先继节点
24 Entry<K,V> after; // 后继节点
25 Entry(int hash, K key, V value, Entry before, Entry after) {
26 this.hash = hash;
27 this.key = key;
28 this.value = value;
29 this.before = before;
30 this.after = after;
31 }
32 }
33 // 查询元素
34 public V get(Object key) {
35 V res = hashMap.get(key);
36 // 如果在已有数据中找到,则将该元素放置到队首
37 if (res != null)
38 moveToFront(key);
39 return res;
40 }
41 // 存入元素/修改元素
42 public void put(K key, V value) {
43 V res = hashMap.put(key,value);
44 // 如果res为null,表示没找到,则存入并放置到队首
45 if (res == null) {
46 Entry<K,V> entry = new Entry<>(key.hashCode(), key, value, null, head);
47 // 如果之前没有头节点
48 if (head == null) {
49 head = entry;
50 tail = entry;
51 } else {
52 // 如果之前有头节点,将头节点before指向entry
53 entry.after = head;
54 head.before = entry;
55 head = entry;
56 }
57 // 判断此时节点数量是否超过最大容量,如果超过,则将队尾元素删除
58 if (hashMap.size() > maxSize) {
59 tail = tail.before;
60 tail.after = null;
61 }
62 } else {
63 // 如果res不为null,表示包含该元素,则将节点放置到队首
64 Entry<K,V> entry = moveToFront(key);
65 // 同时修改节点的V值
66 entry.value = value;
67 }
68 }
69 // 删除元素
70 public void remove(Object key) {
71 V res = hashMap.remove(key);
72 // 如果删除成功,则将链表中节点一并删除
73 if (res != null)
74 removeFromLinkedList(key);
75 }
76 // 将key对应的元素移至队首
77 Entry<K,V> moveToFront(Object key) {
78 // 遍历链表找到该元素
79 Entry<K,V> entry = find(key);
80 // 如果找到了并且不是队首,则将该节点移动到队列的首部
81 if (entry != null && entry != head) {
82 // 如果该节点是队尾
83 if (entry == tail)
84 tail = entry.before;
85 // 先将该节点从链表中移出
86 Entry<K,V> p = entry.before;
87 Entry<K,V> q = entry.after;
88 p.after = q;
89 if (q != null)
90 q.before = p;
91 // 然后将该节点作为新的head
92 entry.before = null;
93 entry.after = head;
94 head = entry;
95 }
96 return entry;
97 }
98 // 将key对应的元素从双端链表中删除
99 void removeFromLinkedList(Object key) {
100 // 遍历链表找到该元素
101 Entry<K,V> entry = find(key);
102 // 如果没找到则直接返回
103 if (entry == null) return;
104 // 如果是队首元素
105 if (entry == head) {
106 // 只有一个节点
107 if (tail == head)
108 tail = entry.after;
109 head = entry.after;
110 head.before = null;
111 } else if (entry == tail) {
112 // 如果是队尾元素
113 tail = tail.before;
114 tail.after = null;
115 }
116 }
117 // 根据key从链表中找对应节点
118 Entry<K, V> find(Object key) {
119 // 遍历链表找到该元素
120 Entry<K,V> entry = head;
121 while (entry != null) {
122 if (entry.key.equals(key))
123 break;
124 entry = entry.after;
125 }
126 return entry;
127 }
128 // 顺序返回元素
129 public List<Entry<K,V>> getList() {
130 List<Entry<K,V>> list = new ArrayList<>();
131 Entry<K,V> p = head;
132 while (p != null) {
133 list.add(p);
134 p = p.after;
135 }
136 return list;
137 }
138 // 顺序输出元素
139 public void print() {
140 Entry<K,V> p = head;
141 while (p != null) {
142 System.out.print(p.key.toString()+":"+p.value.toString()+"\t");
143 p = p.after;
144 }
145 System.out.println();
146 }
147 public static void main(String[] args) {
148 SimpleLRUCache<String, String> test = new SimpleLRUCache(4);
149 test.put("a","1");
150 test.put("b","2");
151 test.put("c","3");
152 test.put("d","4");
153 // 此时顺序为d c b a
154 test.print();
155 // 获取a,此时顺序为 a d c b
156 test.get("a");
157 test.print();
158 // 修改c,此时顺序为 c a d b
159 test.put("c","31");
160 test.print();
161 // 增加e,淘汰末尾元素b,此时顺序为e c a d
162 test.put("e","5");
163 test.print();
164 }
165 }

手写一个LRU工具类的更多相关文章

  1. 【redis前传】自己手写一个LRU策略 | redis淘汰策略

    title: 自己手写一个LRU策略 date: 2021-06-18 12:00:30 tags: - [redis] - [lru] categories: - [redis] permalink ...

  2. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  3. 面试题目:手写一个LRU算法实现

    一.常见的内存淘汰算法 FIFO  先进先出 在这种淘汰算法中,先进⼊缓存的会先被淘汰 命中率很低 LRU Least recently used,最近最少使⽤get 根据数据的历史访问记录来进⾏淘汰 ...

  4. 如何手写一个js工具库?同时发布到npm上

    自从工作以来,写项目的时候经常需要手写一些方法和引入一些js库 JS基础又十分重要,于是就萌生出自己创建一个JS工具库并发布到npm上的想法 于是就创建了一个名为learnjts的项目,在空余时间也写 ...

  5. java中定义一个CloneUtil 工具类

    其实所有的java对象都可以具备克隆能力,只是因为在基础类Object中被设定成了一个保留方法(protected),要想真正拥有克隆的能力, 就需要实现Cloneable接口,重写clone方法.通 ...

  6. 4.redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?

    作者:中华石杉 面试题 redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现? 面试官心理分析 如果你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想当 ...

  7. 手写一个HTTP框架:两个类实现基本的IoC功能

    jsoncat: 仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架 国庆节的时候,我就已经把 jsoncat 的 IoC 功能给写了,具体可以看这篇文章&l ...

  8. 手把手教你手写一个最简单的 Spring Boot Starter

    欢迎关注微信公众号:「Java之言」技术文章持续更新,请持续关注...... 第一时间学习最新技术文章 领取最新技术学习资料视频 最新互联网资讯和面试经验 何为 Starter ? 想必大家都使用过 ...

  9. 浅析MyBatis(二):手写一个自己的MyBatis简单框架

    在上一篇文章中,我们由一个快速案例剖析了 MyBatis 的整体架构与整体运行流程,在本篇文章中笔者会根据 MyBatis 的运行流程手写一个自定义 MyBatis 简单框架,在实践中加深对 MyBa ...

随机推荐

  1. Flask模板注入

    Flask模板注入 Flask模板注入漏洞属于经典的SSTI(服务器模板注入漏洞). Flask案例 一个简单的Flask应用案例: from flask import Flask,render_te ...

  2. webpack核心模块tapable用法解析

    前不久写了一篇webpack基本原理和AST用法的文章,本来想接着写webpack plugin的原理的,但是发现webpack plugin高度依赖tapable这个库,不清楚tapable而直接去 ...

  3. C语言之漫谈指针(上)

    C语言之漫谈指针(上) 在C语言学习的途中,我们永远有一个绕不了的坑,那就是--指针. 在这篇文章中我们就谈一谈指针的一些基础知识. 纲要: 零.谈指针之前的小知识 一.指针与指针变量 二.指针变量的 ...

  4. python课程设计--学生管理系统

    系统要求 1.添加学生 2.删除学生 3.修改学生信息 4.查询学生 5.查看所有学生信息 6.学生信息数据的存储与读取 源码:student.py #coding:utf-8 2 #定义学员类 3 ...

  5. Spring 声明式事务与编程式事务详解

    本文转载自IBM开发者论坛:https://developer.ibm.com/zh/articles/os-cn-spring-trans 根据自己的学习理解有所调整,用于学习备查. 事务管理对于企 ...

  6. Git基本操作流程

    技术背景 Gitee是一款国内的git托管服务,对于国内用户较为友好,用户可以访问Gitee地址来创建自己的帐号和项目,并托管在Gitee平台上.既然是git的托管服务,那我们就可以先看看git的一些 ...

  7. Flutter Widget中的State

    一.Flutter 的声明式视图开发 在原生系统(Android.iOS)或原生JavaScript 开发的话,应该知道视图开发是命令式的,需要精确地告诉操作系统或浏览器用何种方式去做事情. 比如,如 ...

  8. PAT (Advanced Level) Practice 1001 A+B Format (20 分) 凌宸1642

    PAT (Advanced Level) Practice 1001 A+B Format (20 分) 凌宸1642 题目描述: Calculate a+b and output the sum i ...

  9. 如何在CMDB中落地应用的概念?

    如何在CMDB中落地应用的概念? 我们前面讲了应用是整个微服务架构体系下运维的核心,而CMDB又是整个运维平台的基石.今天我就讲讲在CMDB中如何落地应用这个核心概念,以及如何建立应用集群分组的思路. ...

  10. 学习笔记-angular 使用uuid

    import { UUID } from 'angular2-uuid'; let uuid = UUID.UUID().replace(/-/g, '').toLocaleUpperCase(); ...