LRU算法详解(原理及算法代码实现)

LRU算法详解(原理及算法代码实现)-mikechen

LRU算法介绍

LRU的全称为Least Recently Used,翻译出来就是最近最少使用的意思,它是一种内存淘汰算法,当内存不够时,将内存中最久没使用的数据清理掉。

LUR算法是内存管理的一种页面置换算法,就是用来删除内存中不被使用的数据,腾出空间来把常用的数据存进去。

为什么需要LRU算法?

现代计算机内存仍是相当昂贵的,那么如果利用好管理好有限的内存,来为用户提供更好的性能,是一个有意义的议题。

LRU(Least Recently Used) 即最近最少使用,属于典型的内存淘汰机制。

通俗的说,LRU算法认为,最近被频繁访问的数据会具备更高的留存,淘汰那些不常被访问的数据,这样可以极大的节省内存的使用。

所以,LRU算法也就常用于缓存的淘汰策略,更好的提升内存的使用率。

LRU算法原理

LRU算法的实现原理:把所有的缓存数据存入链表中,新插入的或被访问的数据存入链表头,如果链表满了,就把尾部的数据清除。

如下图所示:

LRU算法详解(原理及算法代码实现)-mikechen

为了更好地描述和理解LRU算法的原理,此处我们以一个队列举例,队列的头部表示最久没有被访问的数据,队列的尾部表示最近刚访问的数据。

假如定义一个长度为4的队列,我们随意添加几个数据进去,如下图:

LRU算法详解(原理及算法代码实现)-mikechen

此时我们访问一次下标为1的元素,根据LRU算法的思想,需要将下标为1的元素移动到队列的尾部,表示该元素最近访问过,后面的元素依次前移,如下图:

LRU算法详解(原理及算法代码实现)-mikechen

这样,每次队列满了的时候添加新的元素,我们只需要将队列头部的元素移除掉即可,这样就达到了淘汰最近最少使用数据的功能,这就是LRU算法的基本原理。

LRU算法详解(原理及算法代码实现)-mikechen

这样,每次队列满了的时候添加新的元素,我们只需要将队列头部的元素移除掉即可,这样就达到了淘汰最近最少使用数据的功能,这就是LRU算法的基本原理。

LRU算法实现

上面为了方便理解,我们使用的队列来举的例子,虽然可以实现LRU算法,但性能并不高,因为每次更新队列元素之后,都要对队列里面的元素进行大量拷贝(移动),并且访问队列元素时的时间复杂度是O(n),所以,如果要实现LRU算法,需要再对上面的模型进行优化。

首先,针对元素更新后需要大量拷贝数据的问题,我们可以使用双向链表这种数据结构来解决,因为双向链表中,每个节点都维护有自己的前置节点和后置节点的信息,当我们需要移动一个元素到其他位置时,只需要更新几个节点的前置节点和后置节点信息即可,其他没有涉及的节点数据不需更新,如下图:

LRU算法详解(原理及算法代码实现)-mikechen

然后,针对查询队列元素性能较差的问题,我们可以使用key-value数据结构来解决,因为key-value的查询时间复杂度是O(1),此处我们使用HashMap来实现。实现原理图如下:

LRU算法详解(原理及算法代码实现)-mikechen

双向链表中维护的是我们实际的数据,HashMap中维护的是双向链表中每个节点的key和node之间的关系,这样,我们在查询数据的时候,可以直接根据HashMap中的key找到对应的双向链表中的节点,然后得到节点中的值;并且在更新链表数据时,也只需要更新涉及节点的指向关系即可。

LRU算法实现

LRU的简易代码实现,如下:

public class LRUCache {
    private HashMap<String, LRUNode> map;
    private int capacity;
    private LRUNode head;
    private LRUNode tail;
    public void set(String key, Object value) {
        LRUNode node = map.get(key);
        if (node != null) {
            node = map.get(key);
            node.value = value;
            remove(node, false);
        } else {
            node = new LRUNode(key, value);
            if (map.size() >= capacity) {
                // 每次容量不足时先删除最久未使用的元素
                remove(tail, true);
            }
            map.put(key, node);
        }
        // 将刚添加的元素设置为head
        setHead(node);
    }
    public Object get(String key) {
        LRUNode node = map.get(key);
        if (node != null) {
            // 将刚操作的元素放到head
            remove(node, false);
            setHead(node);
            return node.value;
        }
        return null;
    }
    private void setHead(LRUNode node) {
        // 先从链表中删除该元素
        if (head != null) {
            node.next = head;
            head.prev = node;
        }
        head = node;
        if (tail == null) {
            tail = node;
        }
    }
    // 从链表中删除此Node,此时要注意该Node是head或者是tail的情形
    private void remove(LRUNode node, boolean flag) {
        if (node.prev != null) {
            node.prev.next = node.next;
        } else {
            head = node.next;
        }
        if (node.next != null) {
            node.next.prev = node.prev;
        } else {
            tail = node.prev;
        }
        node.next = null;
        node.prev = null;
        if (flag) {
            map.remove(node.key);
        }
    }
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<String, LRUNode>();
    }
}

 

陈睿mikechen

10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复面试即可获取《史上最全阿里Java面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法