LRU算法介绍
LRU(Least recently used)最近最少使用算法,LRU算法应用非常广泛,在Android开发中最常用的Glide图片加载框架也是使用LRUCache,其核心思想是当缓存数量达到设置值时,移除最近最少使用的数据。
LRU算法实现思路
LRU算法有几个关键点:
- 插入数据和获取数据的时间复杂度是O(1),或者近似O(1)
- 当缓存达到设定值后要删除最近最少使用的数据
第1点,很容易想到使用HashMap
就能满足要求
第2点,每次达到设定值后要删除一个最近最少使用的数据,那说明存储的数据需要有序的,因为删除了该数据之后需要知道最新的最近最少使用的数据。而链表就正好满足有序,且插入删除时间复杂度是O(1)。
因此我们可以结合HashMap
和链表来实现LRU算法:
根据这张图我们来开始实现LRU算法:
我们需要实现put(key, value)
和get(key)
两个方法,图中已经画出了put和get方法经过的路径,但是还有一些细节没有体现:
我们先给出一个规定,链表的尾节点用来存放最近最新使用的数据,接下来开始思考一些细节的部分
get
和put
操作的数据我们需要将其放到链表的尾部,相当于标记其为最近最新使用过;put
方法有两个细节
a. 如果数据之前已经存在,那么修改数据值,并将其放到链表的尾部;
b. 如果数据不存在,那么直接将其插入到链表尾部
LRU算法简单实现(Kotlin)
由于HashMap
算法的实现比较复杂,且不是本文的重点,因此以下使用java中提供的HashMap
来了实现LRU算法。
首先我们先来实现双向链表,为了方便,以下算法的key和value都为int类型
//节点
class Node(var key: Int, var value: Int) {
var pre: Node? = null
var next: Node? = null
}
双向链表实现
定义头尾节点
private val head: Node = Node(0, 0)//哨兵节点,减少一些条件判断
private val tail: Node = Node(0, 0)//哨兵节点
头尾节点都是哨兵节点,不存储实际数据,只是为了实现链表其它方法能减少一些条件判断。
添加到尾节点
fun addLast(node: Node) {
node.pre = tail.pre //1
node.next = tail //2
tail.pre!!.next = node //3
tail.pre = node //4
size++
}
删除节点
fun remove(node: Node) {
node.pre!!.next = node.next //1
node.next!!.pre = node.pre //2
size--
}
//删除第一个节点,即最近最少使用的节点
fun removeFirst(): Node? {
if (isEmpty) {
return null
}
val node = head.next
remove(head.next!!)
return node
}
LRU算法代码
按照上面分析的思路,来实现LRU算法的核心api:get(key)
和put(key, value)
方法
get(key)方法
class Lru(private val cap: Int) {
var hashMap: HashMap<Int, Node> = HashMap()
var dQueue: DQueue = DQueue()
operator fun get(key: Int): Int {
if (!hashMap.containsKey(key)) {
return -1
}
val node = hashMap[key]
makeRecently(node!!)
return node.value
}
private fun makeRecently(node: Node) {
dQueue.remove(node)
dQueue.addLast(node)
}
}
makeRecently(node)
,将节点标记成最近最新使用的节点。首先将当前节点从链表中移除,然后再重新添加到链表尾部;get(key)
,如果节点不存在,直接返回-1,如果存在调用makeRecently(node)
方法标记为最近最新使用的节点并返回node.value
;
put(key, value)
fun put(key: Int, value: Int) {
if (hashMap.containsKey(key)) { //缓存中存在
val node = hashMap[key]
node!!.value = value //更新value
makeRecently(node) //标记为最近最新使用
return
}
if (dQueue.size == cap) { //缓存不存在,并且缓存已满
removeLRU() //删除最近最少使用的节点
}
val node = Node(key, value)
hashMap[key] = node
dQueue.addLast(node) //添加到链表尾部
}
put
方法逻辑多一些,直接看注释就好了。
测试代码
class ExampleUnitTest {
@Test
fun lurTest() {
val lru = Lru(2)
lru.put(2, 1)
lru.put(2, 2)
println(lru.get(2))
lru.put(1, 1)
lru.put(4, 1)
println(lru.get(2))
}
}
结果打印为2,-1。