Leetcode146-LRU缓存的java实现

接触到的一个题目,原题应该是在leetcode。LRU简单按意思来说就是符合最近最少使用的一个队列。我们首先的理解来说,LRU缓存需要几个核心的定义。1. 容量 2. 容器 3. 操作 4. 语义。首先,容量,定义这个LRU队列的长度。而容器是底层采用什么样的数据结构来存储。因为要保证不重复, 首先会想到Hash,而且应该是K-V的形式,Java相关有HashMap。对于操作来说,一个队列,需要有入队和出队,以及初始化的操作,即需要get(), set()等方法。最后一个语义,就是LRU的核心,如何去保证符合最近最少使用。按要求,需要把最近最少使用的放到队尾,最近最新使用的放到队头,引申即需要设置一个头,尾的单独标记,以及操作setHead。注意setHead是整个的难点,要详尽的考虑当前节点是否头节点,是否中间节点,是否尾节点等各种情况,操作实际内容需要是双向链表的形式,设计的核心是考虑各种异常和特殊情况。最初的版本无法通过,目前修改到Accepted,依然有些优化空间,注释有简单思路的介绍,同时代码中包括了默认的几个Case。
以下是代码:

import java.util.HashMap;

/**
    Created by igoso on 18-1-8.
 case 1
 ["LRUCache","put","put","get","put","put","get"]
 [[2],[2,1],[2,2],[2],[1,1],[4,1],[2]]
 -- [null,null,null,2,null,null,-1]

 case 2
 ["LRUCache","put","put","put","put","get","get","get","get","put","get","get","get","get","get"]
 [[3],[1,1],[2,2],[3,3],[4,4],[4],[3],[2],[1],[5,5],[1],[2],[3],[4],[5]]
 -- [null,null,null,null,null,4,3,2,-1,null,-1,2,3,-1,5]

 */
public class LRUCacheDemo {
    public static void main(String[] args) {
        LRUCache cache = new LRUCache(2 /* capacity */);

        /** default case **/
//        cache.put(1,1);
//        cache.put(2,2);
//        cache.put(3,3);
//        cache.put(4,4);
//        cache.get(4);
//        cache.get(3);
//        cache.get(2);
//        cache.get(1);
//        cache.put(5, 5);
//        cache.get(1);
//        cache.get(2);
//        cache.get(3);
//        cache.get(4);
//        cache.get(5);

        /** case 1**/
//        cache.put(1, 1);
//        cache.put(2, 2);
//        cache.get(1);       // returns 1
//        cache.put(3, 3);    // evicts key 2
//        cache.get(2);       // returns -1 (not found)
//        cache.put(4, 4);    // evicts key 1
//        cache.get(1);       // returns -1 (not found)
//        cache.get(3);       // returns 3
//        cache.get(4);       // returns 4

        /** case 2 **/
        cache.put(2,1);
        cache.put(2,2);
        cache.get(2);
        cache.put(1,1);
        cache.put(4,1);
        cache.get(2);


    }
}

class LRUCache {
    private class Node {
        int key;
        int value;
        Node pre;
        Node next;

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

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

    private int capacity;
    private HashMap<Integer, Node> map = new HashMap<>();
    private Node head = null;
    private Node tail = null;

    public LRUCache(int capacity) {
        this.capacity = capacity;
    }

    public int get(int key) {
        if (map.containsKey(key)) {
            Node n = map.get(key);
            setHead(n,false);
            return n.value;
        }

        return -1;
    }

    private void remove(Node node) {
        //是头节点
        if (node.pre != null) {
            node.pre.next = node.next;
        } else {
            head = node.next;
        }

        //是否尾节点
        if (node.next != null) {
            node.next.pre = node.pre;
        } else {
            tail = node.pre;
        }

        //貌似与上面条件重复,可以删除
        if (node == tail) {
            tail = tail.pre;
        }
    }

    private void setHead(Node n, boolean isNew) {
        //如果是头,直接返回
        if (head == n) {
            return;
        }

        //是否插入,注意在put的时候有remove 也算新节点
        if (isNew) {
            n.next = head;
            n.pre = null;

            if (head != null) {
                head.pre = n;
            }

            head = n;
            if (tail == null) {
                tail = head;
            }

            if (tail == head && head.next != null) {
                tail = head.next;
                tail.next = null;
            }
            return;
        }

        //非新插入头,只修改头指针
        //是尾节点
        if (n.next == null) {
            tail = n.pre;
            tail.next = null;
            head.pre = n;//记得给原先头节点的前驱赋当前值
            n.next = head;
            //设置新的头节点
            head = n;
            head.pre = null;

        }else {
            //非尾节点
            head.pre = n;//记得给原先头节点的前驱赋当前值
            n.next.pre = n.pre;
            n.pre.next = n.next;
            n.next = head;
            head = n;
            head.pre = null;
        }
    }

    public void put(int key, int value) {
        //已经存在
        //先删除,再设置为头
        if (map.containsKey(key)) {
            Node old = map.get(key);
            old.value = value;
            remove(old);
            setHead(old,true);
        } else {
            //不存在新建,包括设置新头和删除溢出节点
            Node created = new Node(key, value);
            if (map.size() >= capacity) {
                map.remove(tail.key);
                remove(tail);
                setHead(created,true);
            } else {
                setHead(created,true);
            }
            map.put(key, created);
        }
    }

    @Override
    public String toString() {
        return this.map.toString();
    }
}


以上,相对来说还是比较复杂的,以下是简单的图解,核心是对头节点和尾节点的操作和标记,每个都有双向的指针。注意头指针和尾指针的特性,头的前驱为空,尾的后驱为空,以及只有一个元素,或者两个元素的情况。
更简单的解法看这篇文章:https://www.jianshu.com/p/c7adab253186

《Leetcode146-LRU缓存的java实现》 lru.png

    原文作者:西5d
    原文地址: https://www.jianshu.com/p/3eeb5e5bd873
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞