LRU算法实现

825 阅读3分钟

LRU算法介绍

LRU(Least recently used)最近最少使用算法,LRU算法应用非常广泛,在Android开发中最常用的Glide图片加载框架也是使用LRUCache,其核心思想是当缓存数量达到设置值时,移除最近最少使用的数据。

LRU算法实现思路

LRU算法有几个关键点:

  1. 插入数据和获取数据的时间复杂度是O(1),或者近似O(1)
  2. 当缓存达到设定值后要删除最近最少使用的数据

第1点,很容易想到使用HashMap就能满足要求

第2点,每次达到设定值后要删除一个最近最少使用的数据,那说明存储的数据需要有序的,因为删除了该数据之后需要知道最新的最近最少使用的数据。而链表就正好满足有序,且插入删除时间复杂度是O(1)。

因此我们可以结合HashMap和链表来实现LRU算法:

根据这张图我们来开始实现LRU算法:

我们需要实现put(key, value)get(key)两个方法,图中已经画出了put和get方法经过的路径,但是还有一些细节没有体现:

我们先给出一个规定,链表的尾节点用来存放最近最新使用的数据,接下来开始思考一些细节的部分

  1. getput操作的数据我们需要将其放到链表的尾部,相当于标记其为最近最新使用过;
  2. 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。