LRU CaChe(内存替换算法)

六、LURCache

0、LUR Cache概念

LRU是Least Recently Used的缩写,意思是最近最少使用,它是一种Cache替换算法。 什么是Cache?狭义的Cache指的是位于CPU和主存间的快速RAM,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。广义上的Cache指的是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构。除了CPU与主存之间有Cache,内存与硬盘之间也有Cache,乃至在硬盘与网络之间也有某种意义上的Cache── 称为Internet临时文件夹或网络内容缓存等。Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时,就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。LRU Cache 的替换原则就是将最近最少使用的内容替换掉。其实, LRU译成最久未使用会更形象, 因为该算法每次替换掉的就是一段时间内最久没有使用过的内容。

2、LRU Cache的实现

实现LRU Cache的方法和思路很多,但是要保持高效实现O(1)的put和get,那么使用双向链表和哈希表的搭配是最高效和经典的。使用双向链表是因为双向链表可以实现任意位置O(1)的插入和删除,使用哈希表是因为哈希表的增删查改也是O(1)。

3、JDK中类似LRUCahe的数据结构LinkedHashMap

参数说明:

  1. initialCapacity 初始容量大小:使用无参构造方法时,此值默认是16。
  2. loadFactor 加载因子:使用无参构造方法时,此值默认是 0.75f。
  3. accessOrder: false表示基于插入顺序;true表示基于访问顺序。
java 复制代码
public static void main(String[] args) {
    // 当accessOrder为false时:
    Map<String, String> map = new LinkedHashMap<>(16,0.75f,false);
    map.put("1", "a");
    map.put("2", "b");
    map.put("4", "e");
    map.put("3", "c");
    System.out.println(map);
}
// 输出结果:
// {1=a, 2=b, 4=e, 3=c}
// 以上结果按照插入顺序进行打印
java 复制代码
 public static void main(String[] args) { 
     Map map = new LinkedHashMap<>(16,0.75f,true); 
     map.put("1", "a"); 
     map.put("2", "b"); 
     map.put("4", "e"); 
     map.put("3", "c"); 
     map.get("1"); 
     map.get("2"); 
     System.out.println(map); 
 } 
/*
输出结果:
{4=e, 3=c, 1=a, 2=b}
每次使用get方法,访问数据后,会把数据放到当前双向链表的最后。

当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾;
当accessOrder为默认值false时,从源码中可以看出recordAccess方法什么也不会做。
*/

4、LRUCache结构的特点

双向链表的头节点是最近最少使用的元素,尾节点是最近最长用的节点。

LRUCache(Least Recently Used Cache)是一种常用的缓存淘汰策略,它的主要特点如下:

  1. 容量限制:LRUCache有固定的容量上限,当缓存满时需要淘汰最久未使用的数据。
  2. 快速访问:LRUCache需要能够快速地查找、插入和删除缓存项,通常使用哈希表(字典)和双向链表的组合实现。
  3. 访问顺序跟踪:LRUCache需要记录每个缓存项的访问顺序,以便快速找到最久未使用的项。
  4. 最近最少使用:当缓存满时,LRUCache会淘汰最近最少被访问的缓存项。这样可以保留最有价值的数据。
  5. 时间复杂度:基于哈希表和双向链表的LRUCache实现,可以达到平均时间复杂度O(1)的增删查操作。

典型的LRUCache实现如下:

  1. 使用一个哈希表(字典)存储键值对,便于快速查找。
  2. 使用一个双向链表维护访问顺序,最近访问的节点放在链表头部。
  3. 当缓存已满时,将链表尾部(最久未使用)的节点移除。
  4. 当访问一个缓存项时,将其移动到链表头部。
  5. 当添加一个新缓存项时,将其加到链表头部。

这种结构可以高效地实现LRU缓存的所有操作,是LRU缓存广泛使用的基础。

5、手搓LRUCaChe

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache extends LinkedHashMap<Integer, Integer>{

    public int capacity;
    public LRUCache(int capacity) {
        //这个的true  代表 基于访问顺序
        super(capacity,0.75F,true);
        this.capacity = capacity;
    }

    @Override
    public Integer get(Object key) {
        return super.getOrDefault(key,-1);
    }

    @Override
    public Integer put(Integer key, Integer value) {
        return super.put(key, value);
    }

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


    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(3);
        lruCache.put(100,10);
        lruCache.put(110,11);
        lruCache.put(120,12);
        System.out.println(lruCache);
        System.out.println("获取元素");
        System.out.println(lruCache.get(110));
        System.out.println(lruCache);
        System.out.println(lruCache.get(100));
        System.out.println(lruCache);

        System.out.println("存放元素,会删除头节点,因为头节点是最近最少使用的: ");
        lruCache.put(999,99);

        System.out.println(lruCache);

    }

    public static void main3(String[] args) {
        LinkedHashMap<String,Integer> linkedHashMap =
        new LinkedHashMap<>(16,0.7f,true);
        linkedHashMap.put("高博",10);
        linkedHashMap.put("abcd",11);
        linkedHashMap.put("hello",12);
        System.out.println(linkedHashMap);
        System.out.println("获取元素");
        System.out.println(linkedHashMap.get("abcd"));
        System.out.println(linkedHashMap);
        System.out.println(linkedHashMap.get("高博"));
        System.out.println(linkedHashMap);

    }



    //是基于插入顺序
    public static void main1(String[] args) {
        LinkedHashMap<String,Integer> linkedHashMap =
        new LinkedHashMap<>(16,0.7f,false);
        linkedHashMap.put("高博",10);
        linkedHashMap.put("abcd",11);
        linkedHashMap.put("hello",12);
        System.out.println(linkedHashMap);
        System.out.println("获取元素");
        System.out.println(linkedHashMap.get("abcd"));
        System.out.println(linkedHashMap);
    }
}
java 复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * @Author 12629
 * @Description:
 */
public class MyLRUCache {

    static class DLinkNode {
        public int key;
        public int val;
        public DLinkNode prev;
        public DLinkNode next;
        public DLinkNode() {

        }
        public DLinkNode(int key, int val) {
            this.key = key;
            this.val = val;
        }

        @Override
        public String toString() {
            return  "{ key=" + key +", val=" + val+"} ";
        }
    }

    public DLinkNode head;//双向链表的头节点
    public DLinkNode tail;//双向链表的尾巴节点
    public int usedSize;//代表当前双向链表当中 有效的数据个数
    public Map<Integer,DLinkNode> cache;//定义一个map
    public int capacity;//容量


    public MyLRUCache(int capacity) {
        this.head = new DLinkNode();
        this.tail = new DLinkNode();
        head.next = tail;
        tail.prev = head;
        cache = new HashMap<>();
        this.capacity = capacity;
    }

    /**
     * 存储元素
     * 1. 查找当前的这个key 是不是存储过
     * @param key
     * @param val
     */
    public void put(int key,int val) {
        //1. 查找当前的这个key 是不是存储过
        DLinkNode node = cache.get(key);
        //2. 如果没有存储过
        if(node == null) {
            //2.1 需要实例化一个节点
            DLinkNode dLinkNode = new DLinkNode(key,val);
            //2.2 存储到map当中一份
            cache.put(key,dLinkNode);
            //2.3 把该节点存储到链表的尾巴
            addToTail(dLinkNode);
            usedSize++;
            //2.4 检查当前双向链表的有效数据个数 是不是超过了capacity
            if(usedSize > capacity) {
                //2.5 超过了,就需要移除头部的节点
                DLinkNode remNode = removeHead();
                //2.6 清楚cache当中的元素
                cache.remove(remNode.key);
                //2.7 usedSize--;
                usedSize--;
            }
            printNodes("put");
        }else {
            //3. 如果存储过
            //3.1 更新这个key对应的value
            node.val = val;
            //3.2 然后将该节点,移动至尾巴处【因为这个是新插入的数据】
            moveToTail(node);
        }
    }

    /**
     * 移除当前节点到尾巴节点
     * 逻辑:先删除  后添加到尾巴
     * @param node
     */
    private void moveToTail(DLinkNode node) {
        //1. 先删除这个节点
        removeNode(node);
        //2. 添加到尾巴节点
        addToTail(node);
    }

    /**
     * 删除指定节点
     * @param node
     */
    private void removeNode(DLinkNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    /**
     * 添加节点到链表的尾部
     * @param node
     */
    private void addToTail(DLinkNode node) {
        tail.prev.next = node;
        node.prev = tail.prev;
        node.next = tail;
        tail.prev = node;
    }

    private DLinkNode removeHead() {
        DLinkNode del = head.next;
        head.next = del.next;
        del.next.prev = head;
        return del;
    }


    /**
     * 访问当前的key
     *   逻辑:把你访问的节点 放到尾巴
     * @param key
     * @return
     */
    public int get(int key) {
        DLinkNode node = cache.get(key);
        if(node == null) {
            return -1;
        }
        //把最近 最多使用的 放到了链表的尾巴
        moveToTail(node);
        printNodes("get ");
        return node.val;
    }

    public void printNodes(String str) {
        System.out.println(str+": ");
        DLinkNode cur = head.next;
        while (cur != tail) {
            System.out.print(cur);
            cur = cur.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        MyLRUCache lruCache = new MyLRUCache(3);
        lruCache.put(100,10);
        lruCache.put(110,11);
        lruCache.put(120,12);
        System.out.println("获取元素");
        System.out.println(lruCache.get(110));
        System.out.println(lruCache.get(100));
        System.out.println("存放元素,会删除头节点,因为头节点是最近最少使用的: ");
        lruCache.put(999,99);
    }
}
相关推荐
神仙别闹27 分钟前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭1 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
XH华1 小时前
初识C语言之二维数组(下)
c语言·算法
暮湫1 小时前
泛型(2)
java
超爱吃士力架1 小时前
邀请逻辑
java·linux·后端
南宫生1 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石1 小时前
12/21java基础
java
不想当程序猿_1 小时前
【蓝桥杯每日一题】求和——前缀和
算法·前缀和·蓝桥杯
李小白662 小时前
Spring MVC(上)
java·spring·mvc
落魄君子2 小时前
GA-BP分类-遗传算法(Genetic Algorithm)和反向传播算法(Backpropagation)
算法·分类·数据挖掘