大厂面试必备:LRU算法-删除最近最未少使用算法(详细附源代码)

LRU算法

什么是LRU算法

LRU算法又叫删除最近最未使用算法,是一种缓存淘汰策略。

计算机中的容量是有限的,如果内存满了的话,那么就要删除旧的数据来满足让新数据可以填充进入,那么问题来了,什么样的数据就是要被删除的数据?

LRU缓存算法是一种常用的策略。全名又称Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。

在这里插入图片描述

使用场景

比如说现在有一个鞋柜,里面可以存放6双鞋,每新买一双鞋,你都要将新鞋存放进入,每次存放的时候,你都会判断一下是否有空余的位子,如果有空位子的话,你就可以成功存放进去,如果没有的话,你就需要忍痛割爱,把最近未穿(时间最长)的那双鞋拿出来,给新鞋腾出位置,让新鞋入库。这个就是典型的LRU设计思路。
希望我拿鞋举例子,可以让你们对LRU算法有一定的了解。相信你们一定可以的。
在这里插入图片描述

算法设计思路

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  • 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。

  • 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

在这里插入图片描述
  如图所示,在双向链表中头部数据总为活跃度最高的节点,尾部为活跃度最低的节点,若容器已满的话,首先会淘汰尾部节点,将新数据插入头部,以此保证头部总是活跃度最高的节点。

具体实现

  1. 首先定义数据结构,存放该节点的k值,v值,前继节点和后继节点
/**数据结构*/
    class CacheNode {
    
    
        Object key; // 键
        Object value; // 值
        CacheNode next; // 后继节点
        CacheNode pre; // 前继节点
    }
  1. 定义全局变量:容器(caches),容量大小(capacity),容器中头节点(head)和尾节点(tail)
 /**缓存容器*/
    HashMap<K, CacheNode> caches;

    /**容量大小*/
    private int capacity;
    /**头结点*/
    private CacheNode head;
    /**尾节点*/
    private CacheNode tail;
  1. 初始化容器大小,通过构造方法进行初始化
/**实例化*/
    public LRUCache(int size) {
    
    
        // 容器大小
        this.capacity = size;
        // 实例化容器
        caches = new HashMap<K, CacheNode>(size);
    }
  1. 编写put方法
/**
     *  添加k-v
     * @param k 键
     * @param v 值
     * @return 值
     */
    public V put(K k, V v) {
    
    
        // 1. 从容器中查找是否存在
        CacheNode cacheNode = caches.get(k);
        // 2. 若存在于容器中 将CacheNode节点移到容器队首
        // 3. 若不存在与容器中
        if (cacheNode == null) {
    
    
            // 3.1 容器实际大小 是否大于 所允许存放的最大数量
            if (caches.size() >= capacity) {
    
    
                // 3.1.1 若容器实际大小大于所允许存放的最大数量 将容器尾部的CacheNode节点(活跃度不高的节点)删除
                caches.remove(tail.key);
                // 将tail节点指向它前一个节点  更新tail节点的指向
                removeLast();
            }
            // 3.1.2 若容器实际大小小于所允许存放的最大数量
            // 3.1.2.1 将其封装成CacheNode节点 投入到容器中
            cacheNode = new CacheNode();
            cacheNode.key = k;
        }
        cacheNode.value = v;
        // 将节点 移到 容器头部 保证 容器中的节点是按活跃度排序的
        moveToFirst(cacheNode);
        // 将当前节点封装成CacheNode 填充到容器中
        caches.put(k ,cacheNode);
        return v;
    }
  1. 编写get方法
/**
     *  获取该节点
     * @param k
     * @return
     */
    public V get(K k) {
    
    
        // 查询该k 是否存在于容器中
        CacheNode node = caches.get(k);
        if (node == null) {
    
    
            return null;
        }
        // 将该节点移到容器头部
        moveToFirst(node);
        return (V)node.value;
    }
  1. 自定义的方法
    moveToFirst()方法
/**
     *  将CacheNode移到容器头部 & 更新head和tail节点的指向
     *  0. 若当前节点等于head节点 无需移到 直接return
     *  1. 若当前节点的next节点不为空 将当前节点的后继节点的前继节点指向当前节点的前继节点
     *  2. 若当前节点的pre节点不为空 将当前节点的前继节点的后继节点指向当前节点的后继节点
     *  3. 将tail节点等于当前节点 更新tail节点指向 将tail节点指向当前节点的前继节点
     *  4. 若head和tail节点都为空 直接将当前节点赋值给head和tail节点 -- 表示第一次添加k-v键值对
     * @param cacheNode
     */
    private void moveToFirst(CacheNode cacheNode) {
    
    
        // 若当前节点等于head节点 无需移到 直接return
        if (head == cacheNode) return;
        // 若当前节点的next节点不为空 将当前节点的后继节点的前继节点指向当前节点的前继节点
        if (cacheNode.next != null) cacheNode.next.pre = cacheNode.pre;
        // 若当前节点的pre节点不为空 将当前节点的前继节点的后继节点指向当前节点的后继节点
        if (cacheNode.pre != null) cacheNode.pre.next = cacheNode.next;
        // 将tail节点等于当前节点 更新tail节点指向 将tail节点指向当前节点的前继节点
        if (tail == cacheNode) tail = cacheNode.pre;
        // 若head和tail节点都为空 直接将当前节点赋值给head和tail节点 -- 表示第一次添加k-v键值对
        if (head == null || tail == null) {
    
    
            head = tail = cacheNode;
            return;
        }
        // 将当前节点的next指向head节点
        cacheNode.next = head;
        // head节点的前继节点指向当前节点
        head.pre = cacheNode;
        // 将当前节点赋值给head节点
        head = cacheNode;
        // 将当前节点的前继节点置为空
        head.pre = null;
    }

removeLast()方法

/**
     * 将tail节点指向tail.pre
     */
    private void removeLast() {
    
    
        if (tail != null) {
    
    
            // 将tail节点更新为它的前继节点
            tail = tail.pre;
            // 若tail节点为空 表示容器中无节点 将head置为空
            if (tail == null) head = null;
            // 否则 将tail节点的next置为空
            else tail.next = null;
        }
    }

toString()方法

@Override
    public String toString() {
    
    
        StringBuilder sb = new StringBuilder();
        CacheNode node = head;
        while (node != null) {
    
    
            sb.append(String.format("%s:%s ", node.key, node.value));
            node = node.next;
        }

        return sb.toString();
    }
  1. 代码测试
    编写测试方法
public static void main(String[] args) {
    
    
        LRUCache<Integer, String> lru = new LRUCache<>(3);
        lru.put(1, "a");
        System.out.println(lru.toString());
        lru.put(2, "b");    // 2:b 1:a
        System.out.println(lru.toString());
        lru.put(3, "c");    // 3:c 2:b 1:a
        System.out.println(lru.toString());
        lru.put(4, "d");    // 4:d 3:c 2:b
        System.out.println(lru.toString());
        lru.put(1, "aa");   // 1:aa 4:d 3:c
        System.out.println(lru.toString());
        lru.put(2, "bb");   // 2:bb 1:aa 4:d
        System.out.println(lru.toString());
        lru.put(5, "e");    // 5:e 2:bb 1:aa
        System.out.println(lru.toString());
        lru.get(1);         // 1:aa 5:e 2:bb
        System.out.println(lru.toString());
    }
  1. 结果如图所示
    在这里插入图片描述
    到此,LRU算法已大功告成,恭喜你们,又掌握一门算法,离大厂又进了一步。奥利给 !!!

在这里插入图片描述

公众号

领取Java面试资料的,关注公众号回复关键字【888】,即可领取大厂面试攻略
在这里插入图片描述
代码已收录到github中,如有需要,可自行下载
https://github.com/memo012/java_memo

猜你喜欢

转载自blog.csdn.net/qq_41066066/article/details/106982229
今日推荐