当前位置:   article > 正文

缓存淘汰算法——LRU算法详细总结及代码实现_靠二维数组同时实现 哈希表+lru缓存淘汰 的功能

靠二维数组同时实现 哈希表+lru缓存淘汰 的功能

什么是LRU算法?

LRU是一种缓存淘汰策略,对于我们的计算机来说,缓存容量是有限的,那么当缓存满了要怎么办?这时就要用到LRU,对于很久没有用过的数据,我们可以将其判定为无用的数据,当新资源进入缓存或者用到了某个数据的时候,对应的资源可以判定为有用的数据,当缓存满了时我们应当优先淘汰无用的数据,而对于最近使用过的数据应当靠后淘汰,这就是LRU算法。
和手机后台一样,我们会优先杀掉自己很久没用的后台,而常用的则不会杀掉它,甚至还会上锁

实现思路

  • 设计一个LRUCache数据结构,有put和get方法,并且时间复杂度为O(1)
  • 每次放入或使用数据时,就把这个数据放到第一位,代表近期使用过
  • 当缓存容量满时,再放入数据,优先删除最后的数据,把新数据放第一位
  • 因为对时间复杂度要求较高,我们首先想到的就是使用HashMap,又因为HashMap是无序的,所以我们还需要一个链表并标记头尾支撑有序性,为了方便操作,该链表可设计为双向链表,总的来说,就是使用哈希表和双向链表来设计LRUCache
  • 当放入的数据的key已经存在时,首先在双向链表中删除该结点,然后将这个数据放到第一位,最后对HashMap放入这个数据覆盖掉之前的
  • 当LRUCache已满时,先删除链表的尾部数据,然后删除HashMap的对应数据,再把新数据放到链表头部,最后放入HashMap中

图解

在这里插入图片描述

代码实现

双向链表Node类

属性包括key、value、前驱结点和后继节点

public class Node {
    int key,val;
    Node pre,next;

    public Node(int key, int val) {
        this.key = key;
        this.val = val;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

有序双向链表DoubleList类

为了达到有序性,定义头结点head和尾结点tail,因为缓存有容量,所以还有size属性,表示当前链表中元素的数量。我们先不写出最后四个方法,接下来我们会依次讲解这四个方法

public class DoubleList {
    private Node head,tail;
    private int size;

    public DoubleList() {
        size = 0;
    }

    public void addFirst(Node node){}

    public void romove(Node node){}

    public Node romoveLast(){}

    public int size(){}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

addFirst方法

该方法的作用是将新数据放到链表的头部,利用头插法,时间复杂度为O(1)

    public void addFirst(Node node){
    	//如果此时链表中没有数据,直接将head指向该结点
        if(size == 0){
            head = node;
        }else{ //如果有数据
        	//使用头插法将该结点放到开头,
            node.next = head;
            head = node;
            //注意这是双向链表,还要管理pre指针
            head.next.pre = head;
            //当链表中已经有一个数据时,我们把tail指针加入进来,指向head的下一个
            if(size == 1){
                tail = head.next;
            }
        }
        //链表数据量加1
        size++;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

①如果没有数据,此时head指向node
在这里插入图片描述
②如果已经有一个数据,头插法完毕后,加入tail结点
在这里插入图片描述
③如果数据量大于1,正常的头插法
在这里插入图片描述

romove方法

该方法的作用是移除对应的结点,利用next和pre指针,时间复杂度为O(1)

	public void romove(Node node){
		//如果该结点是尾结点
        if(node.next == null && node.pre != null){
        	//改变尾结点到上一个结点
            tail = node.pre;
            node.pre.next = null;
            node.pre = null;
        //如果该结点是头结点
        }else if(node.pre == null && node.next != null){
        	//改变头结点到下一个结点
            head = node.next;
            node.next.pre = null;
            node.next = null;
        //如果该结点是中间结点
        }else if(node.pre != null && node.next != null){
        	//改变上个结点的next
            node.pre.next = node.next;
            //改变下个结点的pre
            node.next.pre = node.pre;
        //如果该结点是链表中唯一个结点
        }else{
        	//直接把head搞成null
            head = null;
        }
        //链表数据量减1
        size--;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

romoveLast方法

该方法的作用是移除最后的结点,相当于淘汰掉缓存中最不常用的数据,时间复杂度为O(1)
return该结点是因为在HashMap中也要移除,所以要确定该结点的key

    public Node romoveLast(){
        Node node;
        //如果只有一个数据
        if(size == 1){
        	//记住head结点,然后指向null
            node = head;
            head = null;
        }else{ //如果有多条数据
        	//记住tail结点并改变tail的位置
            node = tail;
            tail = tail.pre;
            tail.next = null;
        }
        //链表数据量减1
        size--;
        return node;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

size方法

为了判断LRUCache是否已满,没什么好说的,直接return size

    public int size() {
        return size;
    }
  • 1
  • 2
  • 3

LRU缓存LRUCache类

属性包括了,缓存容量capacity,双向链表list以及哈希表map。对于缓存来说,最基本的方法就是put和get方法,所以我们只要实现这两个方法就可以了,后面来依次讲解

public class LRUCache {
    int capacity;
    DoubleList list;
    HashMap<Integer,Node> map;
	//初始化
    public LRUCache(int capacity) {
        this.capacity = capacity;
        list = new DoubleList();
        map = new HashMap<>();
    }

	public void put(int key,int value){}
	
    public int get(int key){}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

put方法

此方法的作用是将相应的数据放入缓存
注意要放到第一位并且要考虑重复和缓存满的情况

    public void put(int key,int value){
    	//new一个Node存放key和value
        Node node = new Node(key,value);
        //如果HashMap中存在这个key,表示重复了
        if (map.containsKey(key)) {
        	//首先在双向链表中移除这个重复的结点
            list.romove(map.get(key));
            //然后将新结点放到第一位
            list.addFirst(node);
            //最后覆盖HashMap中的相同key的数据
            map.put(key,node);
        //如果HashMap不存在这个key
        }else{ 
        	//如果缓存已满
            if(capacity == list.size()){
            	//移除最后一个数据,这个数据是很久没有被使用过的
                Node last = list.romoveLast();
                //HashMap中也要移除
                map.remove(last.key);
            }
            //将新结点放到第一位
            list.addFirst(node);
            //HashMap中也要存放这个数据
            map.put(key,node);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

get方法

此方法的作用是获取某一key值的数据

    public int get(int key){
    	//如果在哈希表中没有这个key,return -1
        if (!map.containsKey(key)) {
            return -1;
        }
        //如果存在key,通过HashMap的get()获取到相应的结点并得到val
        int res = map.get(key).val;
       	//因为我们使用到了这个数据,所以要调用put()把这个数据放到第一位
        put(key,res);
        return res;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

测试LRUCache

缓存满测试

public class Test {
    public static void main(String[] args){
        LRUCache cache = new LRUCache(2);
        cache.put(1,1);
        cache.put(2,2);
        cache.put(3,3);
        System.out.println(cache.get(1));
        System.out.println(cache.get(2));
        System.out.println(cache.get(3));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

容量为2,key为1的数据是最不常用的数据,应该淘汰,key为3的数据放入
在这里插入图片描述

重复key测试

public class Test {
    public static void main(String[] args){
        LRUCache cache = new LRUCache(2);
        cache.put(1,1);
        cache.put(2,2);
        cache.put(1,4);
        cache.put(3,3);
        System.out.println(cache.get(1));
        System.out.println(cache.get(2));
        System.out.println(cache.get(3));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

容量为2,存在key为1的数据重复,覆盖掉,此时key为2的数据是最不常用的数据,应当淘汰,key为3的数据放入
在这里插入图片描述
就简单测试一下吧,更复杂的测试大家可以自己来进行,本人能力有限,可能出现考虑不周的地方,如果遇到不对的地方希望可以多多指正,谢谢大家

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/163669
推荐阅读
相关标签
  

闽ICP备14008679号