LRU算法原理介绍及其实现方式

451 阅读4分钟

引言

在现代计算机系统中,缓存机制对于提升系统性能至关重要。其中,LRU(Least Recently Used,最近最少使用)算法作为一种经典的缓存淘汰策略,广泛应用于各种场景,如Redis缓存等。本文将详细阐述LRU算法的基本原理,并通过Java代码示例展示如何利用HashMap和双向链表手写实现LRU算法。

LRU算法概述

LRU算法的核心思想是:当缓存空间不足时,优先淘汰最近最少使用的数据。为了实现这一目标,LRU算法维护了一个双向链表和一个哈希表。双向链表用于记录数据的访问顺序,哈希表则用于实现O(1)时间复杂度的键值对查找。 具体数据结构如图所示:

image.png

直接用个例子进行介绍:

数据 1,2,1,3,2, 链表:【】【】,长度为2,需要依次插入链表中

如果此时缓存中已有【1】【2】,当 3 加入的时候,得把后面的2淘汰,变成【3】【1】

JDK中的LRU实现

Java的LinkedHashMap类实际上已经封装了LRU算法的实现。通过继承LinkedHashMap类并重写removeEldestEntry方法,就可以轻松地创建一个LRU缓存。下面案例代码就是使用Java的LinkedHashMap实现的LRU算法。

public class LRUCacheDemoByLinkHashMap<K,V> extends LinkedHashMap<K,V> {
    private int capacity;

    public LRUCacheDemoByLinkHashMap(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return super.size() > capacity;
    }

    public static void main(String[] args) {
        LRUCacheDemoByLinkHashMap<Object, Object> lruCacheDemo = new LRUCacheDemoByLinkHashMap<>(3);
        lruCacheDemo.put(1, "a");
        lruCacheDemo.put(2, "b");
        lruCacheDemo.put(3, "c");
        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(4, "d");
        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(3, "c");
        System.out.println(lruCacheDemo.keySet());
        lruCache雅虎lruCacheDemo.put(5, "x");
        System.out.println(lruCacheDemo.keySet());
    }
}

在这个示例中,首先创建了一个容量为3的LRU缓存。然后,依次插入数据1、2、3,并输出缓存中的键集合。接着插入新数据4,此时缓存已满,因此最久未使用的数据2被淘汰。接下来,更新已存在的数据3,使其移动到链表头部。最后,我们插入新数据5,导致最久未使用的数据1被淘汰。最终执行结果如图所示:

image.png

自定义手写LRU实现

虽然LinkedHashMap已经提供了LRU算法的实现,但为了更深入地理解其原理,我们可以尝试手写一个基于HashMap和双向链表的LRU缓存。以下是实现代码:

public class LRUCacheDemo {

    // map负责查找,构建一个虚拟的双向链表,它里面安装的就是一个Node,作为数据载体
    //1.构造一个Node节点,作为数据载体
    class Node<K,V>{
        K key;
        V value;
        Node<K,V> prev;
        Node<K,V> next;

        public Node() {
            this.prev = this.next = null;
        }

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
    // 2。构造一个双向队列
    class DoubleLinkedList<K,V>{
        Node<K,V> head;
        Node<K,V> tail;

        // 初始化头尾相连,两个伪节点
        public DoubleLinkedList(){
            head = new Node<>();
            tail = new Node<>();
            head.next = tail;
            tail.prev= head;
        }

        //2.1添加到头
        public void addHead(Node<K,V> node){
            // node后指针指向之前 head节点的下一节点(占领)
            node.next = head.next;
            // node的前指针指向头节点
            node.prev = head;
            // 之前 head节点的下一节点 的前指针指向node
            head.next.prev = node;
            // head后指针指向node
            head.next = node;
        }
        // 2。2删除节点
        public void removeNode(Node<K,V> node){
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.prev = null;
            node.next = null;
        }
        //2.3获得最后一个节点
        public Node getLast(){
            return  tail.prev;

        }


    }


    private int cacheSize;
    Map<Integer,Node<Integer,Integer>> map;
    DoubleLinkedList<Integer,Integer> doubleLinkedList;

    public LRUCacheDemo(int cacheSize){
        this.cacheSize = cacheSize;
        map = new HashMap<>();
        doubleLinkedList = new DoubleLinkedList<>();
    }

    public int get(int key){
        if(!map.containsKey(key)){
            return -1;
        }
        Node<Integer,Integer> node = map.get(key);
        // 移动到头部
        doubleLinkedList.removeNode(node);
        doubleLinkedList.addHead(node);

        return node.value;
    }

    public void put(Integer key,Integer value){
        if (map.containsKey(key)){
            Node<Integer,Integer> node = map.get(key);
            node.value = value;
            map.put(key,node);

            //移动到头部
            doubleLinkedList.removeNode(node);
            doubleLinkedList.addHead(node);
        }else{
            if(map.size() == cacheSize){ //坑位满了
                Node<Integer,Integer> lastNode = doubleLinkedList.getLast();
                map.remove(lastNode.key);
                doubleLinkedList.removeNode(lastNode);
            }
            //新增
            Node<Integer,Integer> newNode = new Node<Integer,Integer>(key, value);
            map.put(key,newNode);
            doubleLinkedList.addHead(newNode);
        }
    }


    public static void main(String[] args) {
        LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
        lruCacheDemo.put(1,1);
        lruCacheDemo.put(2,2);
        lruCacheDemo.put(3,3);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(4,4);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3,3);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3,3);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3,3);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(5,5);
        System.out.println(lruCacheDemo.map.keySet());
    }

}

在这个自定义实现中,首先定义了一个Node类,用于表示双向链表中的节点。每个节点包含键、值、前驱节点和后继节点。接着,定义了一个DoubleLinkedList类,用于管理双向链表。该类包含添加节点到头部、删除节点和获取最后一个节点的方法。

LRUCacheDemo类中,使用一个HashMap来存储键值对,以实现O(1)时间复杂度的查找。同时,用一个双向链表来维护元素的访问顺序。当一个元素被访问时,将其移动到双向链表的头部。当需要插入一个新元素时,如果缓存已满,我们会删除双向链表尾部的元素,然后将新元素插入到双向链表头部。

运行结果,如图所示,满足LRU算法原理。 image.png

总结

本文详细介绍了LRU算法的基本原理,并通过JDK的LinkedHashMap类和自定义实现两种方式展示了LRU缓存的实现方法。通过使用HashMap和双向链表,可以在O(1)时间复杂度内完成缓存的访问、修改和淘汰操作。这种高效的缓存管理策略在各种需要缓存数据的场景中具有广泛的应用价值。