大家好,我是安大林,由于最近在准备面试,所以开始刷算法题,LRU算法是一个很经典的算法,考试的频率也比较高,所以刷了一波,在此记录一下,给需要的小伙伴做一个参考啦。

Java中使用的很多中间件中的淘汰策略都有涉及LRU算法,比如redis的缓存过期策略其中就有LRU策略。先看一波LUR算法的简介:

简介

      LRU是Least Recently Used 的缩写,即最近最少使用,常用于置换页面算法,为虚拟页式存储管理服务。 LRU算法的提出,是基于这样一个事实:在前面几条指令中频繁使用的页面很可能在后面的几条指令中频繁使用。 反过来说,已经很久没有使用的很可能在未来较长的一段时间内不会被用到。 这就是著名的局部性原理。此外LRU算法也经常被用作缓存淘汰策略。 本文基于LRU算法的思想,使用Java语言实现一个我们自己的缓存工具类。

实现

本文提供了两种实现方式。

1、简单粗暴的借用LinkHashMap实现,代码比较简单粗暴。

2、使用双向链表加哈希的方式实现。

第一种利用LinkedHashMap添加元素是头插法,自带移除指定元素api,可迭代移除尾部元素三个特性可快捷实现LRU算法,上代码!老铁们这么聪明,一看应该就懂了,不会你来我家!

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class LRU3 {

    /**
     * 利用linkHashMap添加元素会直接添加到末尾的特性
     */
    LinkedHashMap<String, String> map;
    /**
     * 缓存淘汰阈值
     */
    int limit;
    /**
     * 当前缓存数据
     */
    int size;

    LRU3(int limit) {
        this.limit = limit;
        map = new LinkedHashMap<>();
    }

    public void put(String key, String val) {
        String cacheVal = map.get(key);
        if (cacheVal == null && size >= limit) {
            //移除末尾数据
            removeTopEle();
            size--;
        } else {
            //利用linkHashMap自带特性直接移除即可
            map.remove(key);
        }
        //添加到队列尾部
        map.put(key, val);
        size++;
    }

    private void removeTopEle() {
        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
        Map.Entry<String, String> next = iterator.next();
        map.remove(next.getKey());
    }


    public String get(String key) {
        String val = map.get(key);
        if (val == null) {
            return null;
        }
        map.remove(key);
        map.put(key, val);
        return val;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }

    public static void main(String[] args) {
        LRU3 lru3 = new LRU3(1);
        lru3.put("key1", "key1");
        lru3.put("key2", "key2");
        System.out.println(lru3.get("key1"));
        System.out.println(lru3.get("key2"));
        System.out.println(lru3.size());
        System.out.println(lru3.isEmpty());
    }
}

第二种方法使用自定义双向链表和哈希实现
整体思路:利用双向链表的前后指针引用结构来记录数据添加时的顺序,通过头插法,向链表中添加节点,这样新的节点会始终保证在头部,旧的节点在尾部,当已添加元素已经到达预订的阈值时,可以直接从尾部进行数据淘汰,直接取末尾节点即可(满足LRU算法的原则)。取出末尾节点之后,还需要根据末尾节点的值,移除掉哈希中对应的key-val。使用哈希的作用,是为了满足LRU在取元素时可以高效率的取。(O(1)时间复杂度)。基本思路就是酱紫了,下面是具体的代码实现:

import java.util.HashMap;

public class LRU2 {

    HashMap<String, Node> cache = new HashMap<>();
    Node head;
    Node tail;
    int size = 0;
    int limit;

    LRU2(int limit) {
        this.limit = limit;
        head = new Node();
        tail = new Node();
        //首尾相连初始化双向链表
        head.next = tail;
        tail.pre = head;
    }

    public void put(String key, String val) {
        Node cacheNode = cache.get(key);
        if (null == cacheNode) {
            if (size >= limit) {
                //超限,移除尾部节点
                Node delNode = removeNodeTail();
                cache.remove(delNode.key);
                size--;
            }
            Node newNode = new Node(key, val);
            //添加新节点到头部
            addNodeToHead(newNode);
            //此处注意,一定要将节点addNodeToHead之后才put到map中,这样放进去的节点是有前后指针引用的,这样后面移除节点时才不会报错
            cache.put(key, newNode);
            size++;
        } else {
            moveToHead(cacheNode);
        }
    }

    private Node removeNodeTail() {
        Node pre = tail.pre;
        removeNode(pre);
        return pre;
    }

    public String get(String key) {
        Node node = cache.get(key);
        if (null == node) {
            return null;
        }
        moveToHead(node);
        return node.val;
    }


    private void moveToHead(Node node) {
        removeNode(node);
        addNodeToHead(node);
    }

    private void removeNode(Node node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    private void addNodeToHead(Node node) {
        head.next.pre = node;
        node.next = head.next;
        head.next = node;
        node.pre = head;
    }

    public static void main(String[] args) {
        LRU2 lru = new LRU2(1);
        lru.put("age", "age");
        lru.put("name", "name");
        System.out.println(lru.get("age"));
        System.out.println(lru.get("name"));
    }
}

以上两种方法只是简单的为了实现算法写的,如果在真实业务场景中需要使用类似LRU算法的,最好抽象一下,定义一套缓存接口,定义好方法,方便扩展。有问题留言,欢迎沟通交流。