算法练习之Java手写LRUCache


前言

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。


要求:你是否可以在 O(1) 时间复杂度内完成这两种操作?

LRUCache是最近最少使用缓存,具体概念不了解的可以自行百度,本篇是java代码的实现


提示:难点在于O(1) 时间复杂度,思路就是Hash表加双向链表,所以基本功一定要扎实。
要熟练掌握双向链表的头插和尾插,删除末尾和头部元素,删除某个节点
本题用虚拟头尾节点的方式

一、LRU实现

代码如下(示例):

LRUCache代码

class LRUCache {
    public int capacity;//容量
    public int size;//当前元素数量
    public DNodeList head;
    public DNodeList tail;
    public Map<Integer,DNodeList> cache=new HashMap<>();;
    
    class DNodeList {
        int key;
        int value;
        DNodeList prev;
        DNodeList next;

        DNodeList(){

        }
        DNodeList(int key,int value){
            this.key=key;
            this.value=value; 
            this.prev=null;
            this.next=null;
        }
    }


    public LRUCache(int capacity) {
        this.capacity=capacity;
        this.size=0;
        head = new DNodeList();
        tail = new DNodeList();
        head.next=tail;
        tail.prev=head;
    }
    
    
    public int get(int key) {
       //从cache里拿
       DNodeList node=cache.get(key);
       if(node==null){
           return -1;
       }
       moveToHead(node);
       return node.value;
    }
    public void moveToHead(DNodeList node){
        //先删除
        removeNode(node);
        //再添加到头部
        addHead(node);
    }
    
    public void put(int key,int value){
        //先判断cache里有没有
         DNodeList node=cache.get(key);
         if(node==null){
            DNodeList newNode=new DNodeList(key,value);
            addHead(newNode);
            cache.put(key,newNode);
            ++size;
            //代表不存在,先判断是否大于容量
            if(size>capacity){
                //删除双向链表尾部的值,并更新cache
                DNodeList temp=removeTail();
                cache.remove(temp.key);
                --size;
            }
         }else{
             //存在改变原值,并放到头部
             node.value=value;
             moveToHead(node);
         }
    }
    public void removeNode(DNodeList node){
        node.prev.next=node.next;
        node.next.prev=node.prev;
    }
    public void addHead(DNodeList node){
        node.prev=head;
        node.next=head.next;
        head.next.prev=node;
        head.next=node;
    }
    public DNodeList removeTail(){
       DNodeList res= tail.prev;
       removeNode(res);
       return res;
    }
}

补充

随着写代码越来越多,越是发现数据结构与算法这门内功是很有必要的,这里补充下尾插法的双向链表
package algorithm.list;

public class DoubleLinkListTailInsert {
    private Node head;
    private Node tail;

    public DoubleLinkListTailInsert() {
        this.head = null;
        this.tail = null;
    }

    //一定要心中有图,尾部插入
    public void addLast(int data){
        Node node=new Node(data);
        //插入尾部。先固定个头节点
        if(tail==null){
            head=node;
        }else{
            tail.next=node;
            node.prev=tail;
        }
        tail=node;
    }
    public void removeLast(){
        if(tail==null){
            new Exception("无此元素");
        }else if(tail.prev==null){//就一个元素
            head=null;
        }else{ //断掉尾节点的前面的后继指针
            tail.prev.next=null;
        }
        //尾指针向前移
        tail=tail.prev;
    }

    public Node deleteNode(int data){
        //遍历
        Node current=tail;
        while (current.data!=data){
            if(current.prev==null){
                System.out.println("无此元素");
                return null;
            }
            current=current.prev;
        }
        //找到后进行删除,当前节点是尾节点,直接调用removeLast
        if(current==tail){
            removeLast();
        }else{
            current.prev.next = current.next;
            if(current==head){
                head=current.next;
                current.next=null;
            }else{
                current.next.prev=current.prev;
            }
        }
        return new Node(data);
    }

    public static void main(String[] args) {
        DoubleLinkListTailInsert dl=new DoubleLinkListTailInsert();
        dl.addLast(1);
        dl.addLast(2);
        dl.addLast(3);
        dl.deleteNode(2);
        dl.addLast(2);
    }
}

class Node{
    public int data;
    public Node prev;
    public Node next;

    public Node(int data) {
        this.data = data;
        this.prev = null;
        this.next = null;
    }
}

二、Redis LRU算法实现

分析Redis LRU实现之前,我们先了解一下Redis缓存淘汰策略。

当Redis内存超出物理内存限制时,内存会频繁的磁盘swap区交换数据,而交换会导致redis对外服务性能的急剧下降,这在生产环境是不允许的。说得更明白些,在生产环境是不允许交换行为的,通过设置maxmemory可限制内存超过期望大小。

当实际需要的内存大于maxmemory时,Redis提供了6种可选策略:

  1. noeviction:不继续提供写服务,读请求可以继续。
  2. volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先淘汰。也就是说没有设置过期时间的key不会被淘汰。
  3. volatile-ttl:也是淘汰设置了过期时间的key,只不过策略不是lru,而是根据剩余寿命的ttl值,ttl越小越优先被淘汰。
  4. volatile-random:同理,也是淘汰设置了过期时间的key,只不过策略是随机。
  5. allkeys-lru:类比volatile-lru,只不过未设置过期时间的key也在淘汰范围。
  6. allkeys-random:类比volatile-random,只不过未设置过期时间的key也在淘汰范围。

采用HashMap + 双向循环链表具有较好的读写性能,但是有没有发现什么问题呢?对,HashMap和链表都存在空间浪费的情况,HashMap本来就很耗内存,双向链表由于需要空间存储指针,两种数据结构空间使用率都不高,这显然很不划算。

咱们来看下Redis是如何实现的。Redis做法很简单:

随机取若干个key,然后按照访问时间排序,淘汰掉最不经常使用的数据。为此,Redis给每个key额外增加了一个24bit长度的字段,用于保存最后一次被访问的时钟(Redis维护了一个全局LRU时钟lruclock:REDIS_LUR_BITS,时钟分辨率默认1秒)。

redis会基于​server.maxmemory_samples​配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值