动手写一个LRU缓存

前言

LRU 是 Least Recently Used 的简写,字面意思则是最近最少使用

通常用于缓存的淘汰策略实现,由于缓存的内存非常宝贵,所以需要根据某种规则来剔除数据保证内存不被占满。

redis的数据淘汰策略中就包含LRU淘汰算法

如何实现一个完整的LRU缓存呢?这个缓存要满足:

  • 这个缓存要记录使用的顺序
  • 随着缓存的使用变化,要能更新缓存的顺序

基于这种特点,可以使用一个常用的数据结构:链表

  • 每次加入新的缓存时都添加到链表的头节点
  • 当缓存再次被使用时移动缓存到头节点
  • 当添加的缓存超过能够缓存的最大时,删除链表尾节点的元素

单链表和双向链表的选择

单链表的方向只有一个,节点中有一个next指针指向后继节点。而双链表有两个指针,可以支持两个方向,每个节点不止有一个next指针,还有一个prev指针指向前驱节点

双向链表需要额外的两个空间来存放前驱节点的指针prev和后继节点指针next,所以,存储相同大小的数据,双向链表需要更多的空间。虽然相比单向链表,双向链表的每个节点多个一个指针空间,但是这样的结构带来了更多的灵活性,在某些场景下非常适合使用这样的数据结构。删除和添加节点操作,双向链表的时间复杂度为O(1)

在单向链表中,删除和添加节点的时间复杂度已经是O(1)了,双向链表还能比单向链表更加高效吗?

先来看看删除操作

在删除操作中有两种情况:

  • 删除给定值的节点
  • 删除给定指针的节点

对于第一种情况,无论是删除给定值或者是给定的指针都需要从链表头开始依此遍历,直到找到所要删除的值

尽管删除这个操作的时间复杂度为O(1),但是删除的时间消耗主要是遍历节点,对应的时间复杂度为O(n),所以总的时间复杂度为O(n)。

对于第二种情况,已经给定了要删除的节点,如果使用单向链表,还得从链表头部开始遍历,直到找到待删除节点的前驱节点。但是对于双向链表来所,这就很有优势了,双向链表的待删除节点种包含了前驱节点,删除操作只需要O(1)的时间复杂度

同理对于添加操作:

我们如果想要在指定的节点前面或者后面插入一个元素,双向了链表就有很大的优势,他可以在O(1)的时间复杂度搞定,而单向链表还需要从头遍历。

所以,虽然双向链表比单向链表需要更多的存储空间,但是双向链表的应用更加广泛,JDK种LinkedHashMap这种数据结构就使用了双向链表

如何实现LRU缓存

单链表实现

下面我们基于单链表给出简单的代码实现:

package com.ranger.lru;

import java.util.HashMap;
import java.util.Map;

/**
 *
 * @author ranger
 * LRU缓存
 *
 */
public class LRUMap<K,V> {

    /**
     * 定义链表节点
     * @author ranger
     *
     * @param <K>
     * @param <V>
     */
    private class Node<K, V> {
        private K key;
        private V value;
        Node<K, V> next;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
        public Node() {

        }

    }

    /**
     * 缓存最大值
     */
    private int capacity;

    /**
     * 当前缓存数量
     */
    private int size;

    /**
     * 缓存链表头节点
     */
    private Node<K,V> head;

    /**
     * 缓存链表尾节点
     */
    private Node<K,V> tail;

    /**
     * 定义带参构造函数,构造一个为空的双向链表
     * @param capacity  缓存最大容量
     */
    public LRUMap(int capacity) {
        this.capacity = capacity;
        head = null;
        tail = null;
        size = 0;
    }

    /**
     * 无参构造函数,初始化容量为16
     */
    public LRUMap() {
        this(16);
    }

    /**
     * 向双向链表中添加节点
     * @param key
     * @param value
     */
    public void put(K key,V value) {
        addNode(key,value);
    }

    /**
     * 根据key获取缓存中的Value
     * @param key
     * @return
     */
    public V get(K key) {
        Node<K,V> retNode = getNode(key);
        if(retNode != null) {
            // 存在,插入头部
            moveToHead(retNode);
            return retNode.value;
        }
        // 不存在
        return null;
    }

    /**
     * 移动给定的节点到头节点
     * @param node
     */
    public void moveToHead(Node<K,V> node) {
        // 如果待移动节点是最后一个节点
        if(node == tail) {
            Node prev = head;
            while(prev.next != null && prev.next != node) {
                prev = prev.next;
            }
            tail = prev;
            node.next = head;
            head = node;
            prev.next = null;

        }else if(node == head){   // 如果是头节点
            return;
        }else {
            Node prev = head;
            while(prev.next != null && prev.next != node) {
                prev = prev.next;
            }
            prev.next = node.next;
            node.next = head;
            head = node;
        }
    }

    /**
     * 获取给定key的节点
     * @param key
     * @return
     */
    private Node<K,V> getNode(K key){
        if(isEmpty()) {
            throw new IllegalArgumentException("list is empty,cannot get node from it");
        }
        Node<K,V> cur = head;
        while(cur != null) {
            if(cur.key.equals(key)) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    /**
     * 添加到头节点
     * @param key
     * @param value
     */
    private void addNode(K key,V value) {
        Node<K,V> node = new Node<>(key,value);
        // 如果容量满了,删除最后一个节点
        if(size == capacity) {
            delTail();
        }
        addHead(node);
    }

    /**
     * 删除最后一个节点
     */
    private void delTail() {
        if(isEmpty()) {
            throw new IllegalArgumentException("list is empty,cannot del from it");
        }
        // 只有一个元素
        if(tail == head) {
            tail = null;
            head = tail;
        }else {
            Node<K,V> prev = head;
            while(prev.next != null && prev.next != tail) {
                prev = prev.next;
            }
            prev.next = null;
            tail = prev;
        } 

        size--;
    }

    /**
     * 链表是否为空
     * @return
     */
    private boolean isEmpty() {
        return size == 0;
    }

    /**
     * 添加节点到头头部
     * @param node
     */
    private void addHead(Node node) {
        // 如果链表为空
        if(head == null) {
            head = node;
            tail = head;
        }else {
            node.next = head;
            head = node;
        }

        size ++;

    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        Node<K,V> cur = head;
        while(cur != null) {
            sb.append(cur.key)
            .append(":")
            .append(cur.value);
            if(cur.next != null) {
                sb.append("->");
            }
            cur = cur.next;
        }
        return sb.toString();
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        LRUMap<String,String> lruMap = new LRUMap(3) ;
        lruMap.put("1","tom") ;
        lruMap.put("2","lisa") ;
        lruMap.put("3","john") ;
        System.out.println(lruMap.toString());
        lruMap.put("4","july") ;
        System.out.println(lruMap.toString());
        lruMap.put("5","jack") ;
        System.out.println(lruMap.toString());
        String value = lruMap.get("3");
        System.out.println(lruMap.toString());
        System.out.println("the value is: "+value);
        String value1 = lruMap.get("1");
        System.out.println(value1);
        System.out.println(lruMap.toString());

    }

}

输出结果:
3:john->2:lisa->1:tom
4:july->3:john->2:lisa
5:jack->4:july->3:john
3:john->5:jack->4:july
the value is: john
null
3:john->5:jack->4:july
LinkedHashMap实现

了解LinkedHashMap的都知道,它是基于链表实现,其中还有一个?accessOrder?成员变量,默认是?false,默认按照插入顺序排序,为?true?时按照访问顺序排序,也可以调用 构造函数传入accessOrder

LinkedHashMap 的排序方式有两种:

  • 根据写入顺序排序。
  • 根据访问顺序排序。

其中根据访问顺序排序时,每次 get 都会将访问的值移动到链表末尾,这样重复操作就能的到一个按照访问顺序排序的链表

我们可以重写LinkedHashMap中的removeEldestEntry方法来决定在添加节点的时候是否需要删除最久未使用的节点

代码实现如下:

public class LRULinkedHashMap<K,V> {

    /**
     * 缓存map
     */
    private LinkedHashMap<K,V> cacheMap;

    /**
     * 当前缓存数量
     */
    private int size;

    /**
     * 构造一个cacheMap,并设置可以缓存的数量
     * @param size
     */
    public LRULinkedHashMap(int size) {
        this.size = size;

        cacheMap = new LinkedHashMap<K,V>(16,0.75F,true) {
            @Override
            // 重写方法,判断是否删除最久没使用的节点
            protected boolean removeEldestEntry(Map.Entry eldest) {
                if (size + 1 == cacheMap.size()){
                    return true ;
                }else {
                    return false ;
                }
            }
        };
    }
    /**
     * 添加缓存
     * @param key
     * @param value
     */
    public void put(K key,V value){
        cacheMap.put(key,value) ;
    }

    /**
     * 获取缓存
     * @param key
     * @return
     */
    public V get(K key){
        return cacheMap.get(key) ;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        Set<Entry<K, V>> entrySet = cacheMap.entrySet();
        for (Entry<K,V> entry : entrySet) {
            sb.append(entry.getKey())
            .append(":")
            .append(entry.getValue())
            .append("<-");
        }

        return sb.toString();
    }

    public static void main(String[] args) {
        LRULinkedHashMap<String,Integer> map = new LRULinkedHashMap(3) ;
        map.put("1",1);
        map.put("2",2);
        map.put("3",3);
        System.out.println(map);
        map.put("4", 4);
        System.out.println(map);
    }

}

原文地址:https://www.cnblogs.com/watertreestar/p/11780242.html

时间: 2024-10-04 02:38:28

动手写一个LRU缓存的相关文章

动手写一个Remoting测试工具

基于.NET开发分布式系统,经常用到Remoting技术.在测试驱动开发流行的今天,如果针对分布式系统中的每个Remoting接口的每个方法都要写详细的测试脚本,无疑非常浪费时间.所以,我想写一个能自动测试remoting接口的小工具InterfaceTester.而且,当分布式系统中的某个remoting接口出现bug时,该小工具可以提交需要模拟的数据,以便在调试remoting服务的环境中,快速定位和解决bug. InterfaceTester运行起来后的效果如下图: 1.如何使用 (1)首

模拟spring - 动手写一个spring AOP

一.前言 AOP (Aspect Oriented Programing) - 面向切面编程,它主要用于日志记录.性能分析.安全控制.事务处理.异常处理等方面. AOP主要使用JDK的反射和动态代理,AOP代理其实是由AOP框架动态生成的一个对象,该对象可作为目标对象使用,AOP代理包含了目标对象的全部方法,但AOP代理的方法与目标对象的方法存在差异:AOP方法在特定切入点添加了增强处理,并回调了目标对象的方法. 动态代理的文章请参考:http://blog.csdn.net/zdp072/ar

死磕 java线程系列之自己动手写一个线程池

欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写的线程池如何测试? 简介 线程池是Java并发编程中经常使用到的技术,那么自己如何动手写一个线程池呢?本文彤哥将手把手带你写一个可用的线程池. 属性分析 线程池,顾名思义它首先是一个"池",这个池里面放的是线程,线程是用来执行任务的. 首先,线程池中的线程应该是有类别的,有的是核心线程,有

自己动手写一个FTP客户端

自己用socket写一个FTP客户端,模拟主动被动模式.(先支持LIST命令) # -*- coding: utf-8 -*- import socket, sys, thread, threading def main_sock(daddr, actions, saddr=()):     if saddr:         try:             sc=socket.create_connection(daddr, 3, saddr)             #print "Now

死磕 java线程系列之自己动手写一个线程池(续)

(手机横屏看源码更方便) 问题 (1)自己动手写的线程池如何支持带返回值的任务呢? (2)如果任务执行的过程中抛出异常了该怎么处理呢? 简介 上一章我们自己动手写了一个线程池,但是它是不支持带返回值的任务的,那么,我们自己能否实现呢?必须可以,今天我们就一起来实现带返回值任务的线程池. 前情回顾 首先,让我们先回顾一下上一章写的线程池: (1)它包含四个要素:核心线程数.最大线程数.任务队列.拒绝策略: (2)它具有执行无返回值任务的能力: (3)它无法处理有返回值的任务: (4)它无法处理任务

操刀 requirejs,自己动手写一个

前沿 写在文章的最前面 这篇文章讲的是,我怎么去写一个 requirejs . 去 github 上fork一下,顺便star~ requirejs,众所周知,是一个非常出名的js模块化工具,可以让你使用模块化的方式组织代码,并异步加载你所需要的部分.balabala 等等好处不计其数. 之所以写这篇文章,是做一个总结.目前打算动一动,换一份工作.感谢 一线码农 大大帮忙推了携程,得到了面试的机会. 面试的时候,聊着聊着感觉问题都问在了自己的“点”上,应答都挺顺利,于是就慢慢膨胀了.在说到模块化

自己动手写一个iOS 网络请求库的三部曲[转]

代码示例:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary 开源项目:Pitaya,适合大文件上传的 HTTP 请求库:https://github.com/johnlui/Pitaya 本系列文章中,我们将尝试使用 NSURLSession 技术构建一个自己的网络请求库. NSURLSession 简介 NSURLSession 是 iOS7 引入的新网络请求接口,在 WWDC2013

动手写一个Remoting接口测试工具(附源码下载)

基于.NET开发分布式系统,经常用到Remoting技术.在测试驱动开发流行的今天,如果针对分布式系统中的每个Remoting接口的每个方法都要写详细的测试脚本,无疑非常浪费时间.所以,我想写一个能自动测试remoting接口的小工具InterfaceTester.而且,当分布式系统中的某个remoting接口出现bug时,该小工具可以提交需要模拟的数据,以便在调试remoting服务的环境中,快速定位和解决bug. InterfaceTester运行起来后的效果如下图: 1.如何使用 (1)首

java单链表的实现自己动手写一个单链表

单链表:单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素.链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据.自己手动写一个单链表:首先,定义一个节点类: package com.wei; public class Link { public int data;// 存放数据 public Link next;// 存放下一个节点 public Link(