LRU算法的Java实现

136 阅读3分钟

一、LRU算法简介

LRU算法的原理讲解,会的可以直接看代码

 LRU,即最近最少使用算法。算法的逻辑是,当数量满了后,再添加新的值后,需要先淘汰一个旧值。
  而这个旧指就是最近最少使用的值。
那么问题就是,如何确认最近最少使用的值?

有人会说,我记录每个值的使用次数和最近使用时间就可以了。 
逻辑上没错,但一方面记录数据太多,另一方面每次都要遍历整个数据结构,复杂度O(n)。

那么有没有更好的方法能够在O(1)时间内得到要淘汰的旧值呢?
答案当然是有,那就是用队列。将每次使用到的值放到队列队首,那么淘汰的值就是队列尾部的值即可。这样就是O(1)了。

但仅仅将值放到队首是不够的,因为值可能已经存在了,那么我们还需要将队列中的该值给删除掉,
O(1)从队列上删除某个已知的节点(暂不考虑得到这个节点),其实带前置节点的单向队列和双向队列均可实现。
但后面的分析表明双向链表更合适。

目前我们已经分析完添加新节点时的数据结构。但是LRU算法的核心是读
读的不仅仅要把数据读出,还要把读的数据从队列中取出放到队首。

首先如何解决O(1)读呢?空间换时间,Map就行,直接维护Key到队列节点的Map。
这样读数据,只需要从Map中就可以获得数据了。Mapget的平均复杂度为O(1)
而通过Map可以直接获得队列节点,从而直接从队列删除然后放到队首的操作,所以双向队列最佳。

从上面的分析可以得出,LRU算法的实现数据结构是双向队列+Map

二、Java代码实现

1、首先定义队列节点,用于实现双向队列。

注意:原本打算直接用LinkedList来作为双向队列Deque,但是看了看源码,1.8的LinkedList删除某个节点的操作是遍历整个队列来进行比对,所以复杂度太高,因此自己实现。不过并不复杂。
class Node{
 String key;
 String value;
 Node pre = null;
 Node next = null;

 Node(String key, String value){
     this.key = key;
     this.value = value;
 }
}

基础代码就是这样,主要就是pre和next分别指向前一个和后一个的指针。key就是作为Map的key,value就是其他要存储的值,有没有随意。

2、定义LRU的基本结构

public class LRUTest {

    Map<String, Node> map = new HashMap<>();
    int size = 0;
    int maxSize;
    Node head;
    Node tail;
    

    LRUTest(int cap){
        this.maxSize = cap;
        head = new Node("head", "head");
        tail = new Node("tail", "tail");
        head.next = tail;
        tail.pre = head;
    }
}

其中Map就是存储已经包含的节点,size是当前存储的元素数量。maxSize就是最大存储数量。head和tail分别是头尾节点。 3、公共方法定义:主要定义将节点从队列中取出和将节点放入队列首。

//从双向链表上删除节点
private void deleteNode(Node node){
    node.next.pre = node.pre;
    node.pre.next = node.next;
}

//把节点放到队首
private void addHead(Node node){
    node.next = head.next;
    head.next = node;
    node.pre = head;
    node.next.pre = node;
}

4、定义add方法,详情见注释

//添加add
public void add(String key, String value){
    Node newNode = new Node(key, value);
    //判断是否存在
    if(map.containsKey(key)){
        //存在就直接取出,然后放到队首
        Node node = map.get(key);
        //取出
        deleteNode(node);

        //放在队首
        addHead(node);

        return;
    }
    //满了就去掉一个
    if(size == maxSize){
        //从map中删除
        map.remove(tail.pre.key);
        //删除最后一个
        deleteNode(tail.pre);
        //数量-1
        size--;
    }
    //添加到队首
    addHead(newNode);
    //添加到Map中
    map.put(key, newNode);
    //数量+1
    size++;
}

5、定义获得数据的方法,有就返回节点,没有就是null

//读取
public Node get(String key){
    if(!map.containsKey(key)){
        return null;
    }

    Node node = map.get(key);

    deleteNode(node);
    addHead(node);

    return node;
}

最终代码

最终的代码如下,测试也已经通过了

public class LRUTest {

    Map<String, Node> map = new HashMap<>();
    int size = 0;
    int maxSize;
    Node head;
    Node tail;

    //节点定义
    class Node{
        String key;
        String value;
        Node pre = null;
        Node next = null;

        Node(String key, String value){
            this.key = key;
            this.value = value;
        }

        @Override
        public String toString() {
            return key + " " + value;
        }
    }

    LRUTest(int cap){
        this.maxSize = cap;
        head = new Node("head", "head");
        tail = new Node("tail", "tail");
        head.next = tail;
        tail.pre = head;
    }

    //添加add
    public void add(String key, String value){
        Node newNode = new Node(key, value);
        //判断是否存在
        if(map.containsKey(key)){
            //存在就直接取出,然后放到队首
            Node node = map.get(key);
            //取出
            deleteNode(node);

            //放在队首
            addHead(node);

            return;
        }
        //满了就去掉一个
        if(size == maxSize){
            //从map中删除
            map.remove(tail.pre.key);
            //删除最后一个
            deleteNode(tail.pre);
            //数量-1
            size--;
        }
        //添加到队首
        addHead(newNode);
        //添加到Map中
        map.put(key, newNode);
        //数量+1
        size++;
    }

    //读取
    public Node get(String key){
        if(!map.containsKey(key)){
            return null;
        }
        Node node = map.get(key);

        deleteNode(node);
        addHead(node);

        return node;
    }

    //遍历
    public void printAll(){
        System.out.println("-------");
        Node node = head;
        while(node.next != tail){
            System.out.println(node.next.toString());
            node = node.next;
        }
        System.out.println("*****");
    }

    //从双向链表上删除节点
    private void deleteNode(Node node){
        node.next.pre = node.pre;
        node.pre.next = node.next;
    }

    //把节点放到队首
    private void addHead(Node node){
        node.next = head.next;
        head.next = node;
        node.pre = head;
        node.next.pre = node;
    }

}

小结

LRU算法的低时间复杂度实现,主要是依靠双向链表和Map来实现的。双向链表实现快速的淘汰元素或者将元素从队列中移到队首。而Map则提供快速的定位查找节点在队列的位置,提升查找速度。(就是空间换时间呗)