LeetCode: LRU Cache 最近最少使用算法 缓存设计

设计并实现最近最久未使用(Least Recently Used)缓存。

 

题目描述:

Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and set.

get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
set(key, value) - Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.

设计并实现最近最久未使用的缓存数据结构,支持 get 和 set 操作.

get()-如果 key 存在,返回对应的 value 值,否则返回 -1.

set()-插入 key 对应的 value 到缓存中,如果缓存已满,将最近最久未使用的元素从缓存中移除。

要实现这个设计,我们先回顾一下大学课堂上的知识。
LRU,即最近最少使用,是操作系统内存管理的一种页面置换算法,
常见的页面置换算法,最佳置换算法(OPT,理想置换算法),先进先出置换算法(FIFO),
最近最久未使用算法(LRU),最少使用算法。

其中,最佳置换算法是一种理想情况下的页面置换算法,实际上不可能实现。该算法的基本思想是发生缺页时,有些页面在内存中,其中有一页将很快被访问(也包含紧接着的下一条指令的那页),而其他页面则可能要到10、100或者1000条指令后才会被访问,每个页面都可以用在该页面首次被访问前所要执行的指令数进行标记。最佳页面置换算法规定标记最大的页应该被置换。但当缺页发生时,操作系统无法知道各个页面下一次是在什么时候被访问。这个算法无法实现,但可以用于对可实现算法的性能进行衡量。

另外两种主要算法,LFU算法-实现缓存,FIFO算法-实现缓存,可以查看这里

LRU的实现方法有很多,传统的LRU实现方法:

1.计数器。最简单的情况是使每个页表项对应一个使用时间字段,并给CPU增加一个逻辑时钟或计数器。每次存储访问,该时钟都加1。每当访问一个页面时,时钟寄存器的内容就被复制到相应页表项的使用时间字段中。这样我们就可以始终保留着每个页面最后访问的“时间”。在置换页面时,选择该时间值最小的页面。
2.栈。用一个栈保留页号。每当访问一个页面时,就把它从栈中取出放在栈顶上。这样一来,栈顶总是放有目前使用最多的页,而栈底放着目前最少使用的页。由于要从栈的中间移走一项,所以要用具有头尾指针的双向链连起来。

(1)使用 LinkedHashMap实现Lrucache

Java语言可以利用 LinkedHashMap, LinkedHashMap 是有序的哈希表,可以保存记录的插入顺序,并且按使用顺序排列。
重写其中的removeEldestEntry(Map.Entry)方法,就可以实现LRU算法。

在Mysql Jdbc Util和Apache的很多Jar包中,都是使用LinkedHashMap实现LRUCache。
下面的代码来自mysql-connector-java-5.1.18-bin.jar


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

package com.mysql.jdbc.util;

 

import java.util.LinkedHashMap;

import java.util.Map;

 

public class LRUCache extends LinkedHashMap

{

 

    public LRUCache(int maxSize)

    {

        super(maxSize, 0.75F, true);

        maxElements = maxSize;

    }

 

    protected boolean removeEldestEntry(java.util.Map.Entry eldest)

    {

        return size() > maxElements;

    }

 

    private static final long serialVersionUID = 1L;

    protected int maxElements;

}

  

不过LeetCode的OJ肯定不支持这样实现,上面的代码修改后提交,提示 Comoile Error 。

(2)使用双向链表实现

JDK中,LinkedHashMap是通过继承HashMap,维护一个双向链表实现,

当某个Cache位置被命中,通过调整链表的指向将该位置调整到头位置,新加入的内容直接放在链表头,在多次进行Cache操作后,最近使用的Cache就会向链表头部移动,链表尾部就是命中次数最少,最久未使用的Cache。
空间充满时,移除尾部的数据就可以了。有几点需要注意,一个是Key不存在的情况,一个是缓存设计要求Key唯一。

下面使用双向链表实现LRU Cache,主要是维护一个缓存设定容量,当前容量,以及双向链表的头尾节点,方便移动和删除。

import java.util.HashMap;
/**
 *  近期最少使用算法 设计缓存
 */
public class LRUCache {

    private int cacheSize;//缓存容量
    private int currentSize;//当前容量
    private HashMap<Object, CacheNode> nodes;//缓存容器
    private CacheNode head;//链表头
    private CacheNode last;//链表尾

    class CacheNode{
        CacheNode prev;//前一节点
        CacheNode next;//后一节点
        int value;//值
        int key;//键
        CacheNode() {
        }
    }

    //初始化缓存
    public LRUCache(int capacity) {
        currentSize=0;
        cacheSize=capacity;
        nodes=new HashMap<Object, CacheNode>(capacity);
    }

    public Integer get(int key) {
        CacheNode node = nodes.get(key);
        if (node != null) {
            move(node);
            return node.value;
        } else {
            return -1;//error code
        }

    }

    public void set(int key, int value) {
        CacheNode node = nodes.get(key);
        //重复Key
        if(node!=null){
            node.value=value;
            move(node);
            nodes.put(key, node);
        }else
           {//key未重复,正常流程
            node =new CacheNode();
            if(currentSize>=cacheSize){
                if (last != null){//缓存已满,进行淘汰
                    nodes.remove(last.key);}
                removeLast();//移除链表尾部并后移
            }else{
                currentSize++;
            }

            node.key=key;
            node.value=value;
            move(node);
            nodes.put(key, node);
        }
    }

    //移动链表节点至头部
    private void move(CacheNode cacheNode){
        if(cacheNode==head)
            return;
        //链接前后节点
        if(cacheNode.prev!=null)
            cacheNode.prev.next=cacheNode.next;
        if(cacheNode.next!=null)
            cacheNode.next.prev=cacheNode.prev;
        //头尾节点
        if (last == cacheNode)
            last = cacheNode.prev;
        if (head != null) {
            cacheNode.next = head;
            head.prev = cacheNode;
        }
        //移动后的链表
        head = cacheNode;
        cacheNode.prev = null;
        //节点唯一的情况
        if (last == null)
            last = head;
    }

    //移除指定缓存
    public void remove(int key){
        CacheNode cacheNode =  nodes.get(key);
        if (cacheNode != null) {
            if (cacheNode.prev != null) {
                cacheNode.prev.next = cacheNode.next;
            }
            if (cacheNode.next != null) {
                cacheNode.next.prev = cacheNode.prev;
            }
            if (last == cacheNode)
                last = cacheNode.prev;
            if (head == cacheNode)
                head = cacheNode.next;
        }

    }
    //删除尾部的结点,即去除最近最久未使用数据
    private void removeLast(){
        if(last!=null){
            if(last.prev!=null){
                last.prev.next=null;
            }else{//空间大小为1的情况
                head = null;
            }
            last = last.prev;
        }
    }

    public void clear() {
        head = null;
        last = null;
    }
    //测试用例
//    public static void main(String[] args){
//        LRUCache lCache=new LRUCache(2);
//        lCache.set(2, 1);
//        lCache.set(1, 1);
//        lCache.set(2, 3);
//        lCache.set(4, 1);
//        System.out.println(lCache.get(1));
//        System.out.println(lCache.get(2));
//
//    }

}

 

时间: 2024-10-03 03:44:50

LeetCode: LRU Cache 最近最少使用算法 缓存设计的相关文章

[LeetCode] LRU Cache

Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and set. get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1. set

浅析LRU(K-V)算法缓存教程

LRU(Least Recently Used)算法是缓存技术中的一种常见思想,顾名思义,最近最少使用,也就是说有两个维度来衡量,一个是时间(最近),一个频率(最少).如果需要按优先级来对缓存中的K-V实体进行排序的话,需要考虑这两个维度,在LRU中,最近使用频率最高的排在前面,也可以简单的说最近访问的排在前面.这就是LRU的大体思想. 在操作系统中,LRU是用来进行内存管理的页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间

哪个Map最适合用来实现LRU Cache?

问题描述 前段时间面试碰到的一道题,最近进了这家公司了,再拿出来看看.求大神继续解脱.30.下面哪个Map最适合用来实现LRUCache?A.HashtableB.TreeMapC.HashMapD.IdentityHashMapE.WeakHashMap网上给的答案是A.为什么?大家一般如何实现这个最近最少使用cache? 解决方案 解决方案二:就那几个可选的来说只能是A了因为是线程安全的不过个人认为应该用Mapmap=Collections.synchronizedMap(newLinked

LRU Cache

Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and set. get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.set(

关于.Net开发下的分布式缓存设计

最近拜读了代振军同学写的关于.net开发下的Discuz!NT的缓存设计的一篇文 章Discuz!NT 缓存设计简析 [原创],颇有些想法,姑且写在这里让大家拍砖吧 .;) 缓存真是个好东西,在大型的系统中可以有效地提升系统的速度,此乃废话 就不多说了,在.Net 平台下面我把缓存从功用大致分为两类,数据对象缓存和 页面输出缓存. 对于数据缓存来讲是由System.Web.Caching.Cache这个类来实现 ,可以从上下文对象Context.Cache 来获取这个对象的引用.而页面/控件输出

.NET 缓存设计的使用说明_实用技巧

关于缓存的设计1.什么情况下用缓存 缓存是提高应用程序性能的最好方法之一.运用缓存可以优化数据查询,避免不必要的网络数据回传,和避免执行不必要的完全相同的数据处理逻辑.在实现缓存的时候我们要确定什么时候装入缓存数据.用异步装入缓存或用批处理方式来避免出现客户端数据延迟.一般来说在一定时间内请求了相同的业务逻辑而没有变更的话,可以采用缓存来设计.数据请求频繁的的请求不适合采用缓存,如论坛的回复,但是论坛的主题是可以采用缓存设计的. 2.缓存设计的步骤确定缓存数据结构:即设计中哪些数据用到了缓存,设

CYQ.Data V5 分布式自动化缓存设计介绍

前方: 其实完成这个功能之前,我就在思考:是先把想法写了来,和大伙讨论讨论后再实现,还是实现后再写文论述自己的思维. 忽然脑后传来一个声音说:你发文后会进入发呆阶段. 所以还是静下心,让我轻轻地把代码撸完再说. 最近这几天,自己在大脑里演练过各种技术难点,解决方案,推敲了各种该解决的问题,觉的差不多了,才决定撸码. 忽然发觉,原来代码是可以写在大脑里的. 要是你看到一个员工坐着2天没写一行代码,说明人家是高手,正在大脑编程. 好,不扯,回正文! 传统ORM的二级缓存为何失效? 有些ORM会提供:

CYQ.Data V5 分布式自动化缓存设计介绍(二)

CYQ.Data V5 分布式自动化缓存设计介绍(二) 前言: 最近一段时间,开始了<IT连>创业,所以精力和写的文章多数是在分享创业的过程. 而关于本人三大框架CYQ.Data.Aries.Taurus.MVC的相关文章,基本都很少写了. 但框架的维护升级,还是时不时的在进行中的,这点从开源的Github上的代码提交时间上就可以看出来了. 毕竟<IT连>的后台WebAPI,用的是Taurus.MVC,后台系统管理用的是Aries. 不过今天,就不写创业相关的文章了,先分享篇技术类

通过请求页式存储管理中页面置换算法模拟设计

问题描述 通过请求页式存储管理中页面置换算法模拟设计 (1)随机产生一个页面走向序列. (2)计算并输出下述各种算法在不同内存容量下的命中率. ①先进先出页面置换算法(FIFO): ②最近最久未使用页面置换算法(LRU): ③最佳淘汰算法(OPT): ④最不经常使用页面淘汰算法(LFU): ⑤最近没有使用页面淘汰算法(NUR).