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. java实现下载器(以及创建一个URL对象)

    java实现下载器(以及创建一个URL对象) 1.思路讲解: (1)注意路径:是网络路径噢 (2)创建创建网路协议对象(远程对象):HttpURLConnection urlConnection (3 ...

  2. 2019 GDUT Rating Contest II : Problem B. Hoofball

    题面: 传送门 B. Hoofball Input file: standard input Output file: standard output Time limit: 5 second Memor ...

  3. switch case语句,switch case用法详解

    switch 是"开关"的意思,它也是一种"选择"语句,但它的用法非常简单.switch 是多分支选择语句.说得通俗点,多分支就是多个 if. 从功能上说,sw ...

  4. 浅析MyBatis(三):聊一聊MyBatis的实用插件与自定义插件

    在前面的文章中,笔者详细介绍了 MyBatis 框架的底层框架与运行流程,并且在理解运行流程的基础上手写了一个自己的 MyBatis 框架.看完前两篇文章后,相信读者对 MyBatis 的偏底层原理和 ...

  5. PAT (Advanced Level) Practice 1019 General Palindromic Number (20 分) 凌宸1642

    PAT (Advanced Level) Practice 1019 General Palindromic Number (20 分) 凌宸1642 题目描述: A number that will ...

  6. Golang+Protobuf+PixieJS 开发 Web 多人在线射击游戏(原创翻译)

    简介 Superstellar 是一款开源的多人 Web 太空游戏,非常适合入门 Golang 游戏服务器开发. 规则很简单:摧毁移动的物体,不要被其他玩家和小行星杀死.你拥有两种资源 - 生命值(h ...

  7. 2020 OO 第二单元总结

    只要跑得够快即使从头关到尾你也喜欢吗? 一.设计策略 1.1 总体策略概述 在多线程的协同和同步控制方面,我三次作业都是采用生产者/消费者模式(还憨憨地在内部分了customer.producer.t ...

  8. 单个java文件打成可执行jar包

    1 概述 使用JDK自带的jar与java将单个java文件打成可执行jar包并运行. 当然也可以使用IDE完成,使用Maven只需要一个简单的package,但是单个文件嘛,没必要这么"凶 ...

  9. Fiddler使用 断点 模拟返回 AutoResponder Mock 模拟数据 相关学习记录

    断点 测试中有时需要改变发出去的请求信息,需要用到打断点的方法.断点包含两种方式: before response:在request请求的时候,未到达服务器之前,一般用来修改请求参数 after re ...

  10. matlab map容器类型

    matlab map容器类型 map容器类型以及map类概述 map是将一个量映射到另一个量上,此是前面的量就是map的键(key),后面的量就是map的数据(value).map的键和对应的数据都储 ...