基于HashMap+双向列表手写实现LRU算法

2024-01-16 15:17:19 浏览数 (1)

前言

本文将介绍LRU(Least Recently Used,最近最少使用)算法的基本概念,以及如何使用Java的LinkHashMap,并且手写自定义实现来实现LRU算法。可能很多人不清楚这个算法,但是如果知道Redis缓存淘汰策略,那么肯定离不开LRU算法,希望能够通过本文讲解,在实际开发中,能用到LRU进行优化

LRU算法介绍

Least Recently Used,最近最少使用,页面置换算法,选择最近最久未使用的数据予以淘汰。LRU算法是一种常用的缓存替换策略,它的核心思想是在缓存空间不足时,优先淘汰最近最少使用的数据。通过维护一个双向链表和一个哈希表,可以在O(1)时间内完成缓存的访问、修改和淘汰操作。简单来说:

最近最少使用,就是把数据加入一个链表中,按访问时间排序,发生淘汰的时候,把访问时间最旧的淘汰掉。

比如有数据 1,2,1,3,2,依次插入链表中

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

JDK利用LinkHashMap实现LRU

首先向从现有API入手,JDK其实也是封装好了对应的API,也就是LinkedHashMap类,使用时继承该类,设置相关参数就可以直接使用了。

如以下代码,设置LinkedHashMap大小为3,先插入数据,a,b,c,继续插入数据的时候,查看LinkedHashMap的数据情况。

代码语言:java复制
public class LRUCacheDemoByLinkHashMap<K,V> extends LinkedHashMap<K,V> {

    // 缓存坑位
    private int capacity;

    /**
     *
     * @param capacity
     */
    public LRUCacheDemoByLinkHashMap(int capacity) {
        super(capacity,0.75F,false);
        this.capacity = capacity;
    }

    /***
     * 删除最老的节点
     * @param eldest
     * @return
     */
    @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());

        System.out.println("插入新数据:");
        lruCacheDemo.put(4,"d");
        System.out.println(lruCacheDemo.keySet());
        System.out.println("插入已存在数据:");
        lruCacheDemo.put(3,"c");
        System.out.println(lruCacheDemo.keySet());


        lruCacheDemo.put(5,"x");
        System.out.println(lruCacheDemo.keySet());

}

结果,可以看到,LinkedHashMap数据从左到右,根据插入时间排序,也就是最右的数据为最新的,但是数据满了之后,会把最旧的数据(最近最久)删除

上述代码也可以看到,有个配置,主要设置链表大小,已经Map的扩容负载因子,可以直接设置0.75(map扩容默认就是0.75),这里介绍一下最后一个参数。

代码语言:java复制
super(capacity,0.75F,false);

最后一个字段accessOrder:

true: 每次插入,如果数据在链表存在,则排到最新(最右)

false:每次插入,如果数据在链表存在,则位置不变

上述代码设置了false,所以插入已存在数据,位置没有变化,接下来试试改成true。

可以看到,当插入已存在数据,比如 3,可以看到会将位置重新排序,排到最右。

以上就是关于JDK封装好的LRU实现方案,调用方很简单,主要是对LinkedHashMap的使用。

自定义手写实现LRU

虽然已经有了封装好的API,但是为了加深理解LUR算法,接下模拟LinkedHashMap数据结构,手写LRU框架。

思想:LRU算法目的实现读写两个操作,命中O(1) 时间复杂度

设计方案:采用哈希链表,hashmap 双向列表,主要是是因为哈希查找快,链表插入和删除快,利用hash来存储查找数据, 双向链表关联每个元素。最新最经常使用的排在最右(读取和插入都会最右)

具体结构如下:

删除节点,该节点后一个节点的前后指向该节点前一个节点,设置该节点指向指针为空

插入节点,新节点前后指向头结点

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

代码语言:java复制
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());
    }

}

测试结果:

可以看到最终结果跟JDK自带的LinkedHashMap效果一样。

总结

本文主要是介绍了LRU算法,并且通过演示现有JDK的api,以及基于HashMap 双向链表自定义实现LRU。其思想是,通过使用HashMap,我们可以在不牺牲性能的情况下实现快速的键值对查找。同时,通过使用双向链表,可以维护元素的访问顺序,确保最近被访问的元素始终位于链表的头部,从而实现对LRU缓存的高效管理。这种实现方法适用于各种场景,特别是在需要缓存数据并且需要快速访问的场景中,表现尤为出色。

我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!

0 人点赞