LFU 간단한에서 복잡한 구현 오가지,

머리말

최근 전원 버튼 문제를 브러시, 나를 위해이 0 근거는, 두개골 정말 상처. 제 생각 엔 이번 달 중간 어려운 문제이며, 문제는 간단하지 않다.

다행히, 쓰기 설명에 큰 버클 힘 가축의 다양한있다. 그들은 격차, 정말 부러움을 채우기 위해 코드를 보았다. 나는 대부분의 "달콤한 이모,"친밀한 자매, 또한 이름이 활기 형제라고 감동했다. 거의 매일 나는 반드시보아야 할 자신의 문제 솔루션이었다.

아이디어가 너무 적게 쓰기 때문에 기분이 문제에 달콤한 이모 솔루션, 자세 잘 생긴하지만,하지만 날 위해 초보자는 너무 친절하지, 그리고 매우 상세한 없습니다. 그래서, 시간을 이해하지 못하는 때마다, 그녀의 코드의 개념을 이해하려면 약속을 여러 번 반복했다.

지난 주말 질문은 LFU 캐싱 알고리즘의 실현을 가능하게하는 것입니다. 내 연구의 몇 시간 후 (사실,이 8 시간 이상, 그리고 방법 아 있어야하지 않는다, 음식이 훨씬 더 조금 부지런하다), 달콤한 이모 마침내 모든 생각을 이해합니다. 나중에 쉽게 자신을 검토, 모든 생각을 내려 놓고, 그림과 코드 주석의 큰 숫자와 함께하기 위해, 나는 나 같은 초보자, 매우 친절 있다고 생각합니다.

달콤한 이모 동의 후, 나는이 참조합니다 소스 게시 : https://leetcode-cn.com/problems/lfu-cache/solution/java-13ms-shuang-100-shuang-xiang-lian-biao-duo-ji /

전원 버튼의 시간 복잡도 O (1)을 해결하기 위해 필요하지만, 그렇지 않으면 나는 느낌이 있지만 그것을 이해하는 것이 필요하다, 결국 다시 자신을 달성하기 위해 깊이에 얕은에서 프로세스는 항상 좋은이다. 그래서 나는 다시 한 번 복잡한 모든 반복에 간단한에서 해결하는 방법 오가지을 넣어.

달성 LFU

다음과 같이 스테이 버튼 원제를 설명한다 :

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。它应该支持以下操作:get 和 put。

get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
put(key, value) - 如果键不存在,请设置或插入值。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近 最少使用的键。
「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。

示例:

LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回 1
cache.put(3, 3);    // 去除 key 2
cache.get(2);       // 返回 -1 (未找到key 2)
cache.get(3);       // 返回 3
cache.put(4, 4);    // 去除 key 1
cache.get(1);       // 返回 -1 (未找到 key 1)
cache.get(3);       // 返回 3
cache.get(4);       // 返回 4

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lfu-cache

LFU 얻을 넣어 작업을 방문 빈도를 증가, 제거해야 요소 방문의 수 (액세스 빈도)에 따라 크기를 결정하는 알고리즘을 설계하도록 요청하는 것입니다. 액세스 빈도가 동일 할 때, 적어도 최근에 사용 된, 삭제되는 결정 소자.

따라서이 문제는, 하나는 액세스 빈도이며, 두 가지 측면을 고려할 필요가 액세스 시간의 순서입니다.

프로그램 : 우선 순위 큐를 사용하여

아이디어 :

우리는 PriorityQueue 인 JDK가 제공하는 우선 순위 큐를 사용하여 구현 될 수있다. 내부 우선 순위 큐 각각의 요소 조사 시간은 우리의 요구에 따라 현재의 요소의 최대 값 또는 최소값을 모두 제거 할 수 있도록 할 수있는 이진 힙을 유지하기 때문이다. 우리는 엔티티 클래스가에 Comparable 인터페이스를 구현해야합니다.

따라서, 우리는 크기 비교를 위해, 성장하는 글로벌 인덱스에서, 현재 요소의 액세스 빈도 주파수를 저장 노드를 정의 할 필요가있다. 그런 정보 요소를 저장하기위한지도 <정수 노드> 캐시를 정의한다.

불충분 캐시 용량이있는 경우, 액세스 빈도 주파수의 크기에 따라 최소의 주파수를 제거한다. 동일한 경우 인덱스가 증가하기 때문에, 다음 인덱스가 최소한의 삭제, 더 큰는 최근 액세스, 오랫동안 요소보다 작은이 방문하지 않았습니다.

시간 복잡도 그래서 그것은 본질적 이진 힙 구현 O (logn).

public class LFUCache4 {

    public static void main(String[] args) {
        LFUCache4 cache = new LFUCache4(2);
        cache.put(1, 1);
        cache.put(2, 2);
        // 返回 1
        System.out.println(cache.get(1));
        cache.put(3, 3);    // 去除 key 2
        // 返回 -1 (未找到key 2)
        System.out.println(cache.get(2));
        // 返回 3
        System.out.println(cache.get(3));
        cache.put(4, 4);    // 去除 key 1
        // 返回 -1 (未找到 key 1)
        System.out.println(cache.get(1));
        // 返回 3
        System.out.println(cache.get(3));
        // 返回 4
        System.out.println(cache.get(4));
    }

    //缓存了所有元素的node
    Map<Integer,Node> cache;
    //优先队列
    Queue<Node> queue;
    //缓存cache 的容量
    int capacity;
    //当前缓存的元素个数
    int size;
    //全局自增
    int index = 0;

    //初始化
    public LFUCache4(int capacity){
        this.capacity = capacity;
        if(capacity > 0){
            queue = new PriorityQueue<>(capacity);
        }
        cache = new HashMap<>();
    }

    public int get(int key){
        Node node = cache.get(key);
        // node不存在,则返回 -1
        if(node == null) return -1;
        //每访问一次,频次和全局index都自增 1
        node.freq++;
        node.index = index++;
        // 每次都重新remove,再offer是为了让优先队列能够对当前Node重排序
        //不然的话,比较的 freq 和 index 就是不准确的
        queue.remove(node);
        queue.offer(node);
        return node.value;
    }

    public void put(int key, int value){
        //容量0,则直接返回
        if(capacity == 0) return;
        Node node = cache.get(key);
        //如果node存在,则更新它的value值
        if(node != null){
            node.value = value;
            node.freq++;
            node.index = index++;
            queue.remove(node);
            queue.offer(node);
        }else {
            //如果cache满了,则从优先队列中取出一个元素,这个元素一定是频次最小,最久未访问过的元素
            if(size == capacity){
                cache.remove(queue.poll().key);
                //取出元素后,size减 1
                size--;
            }
            //否则,说明可以添加元素,于是创建一个新的node,添加到优先队列中
            Node newNode = new Node(key, value, index++);
            queue.offer(newNode);
            cache.put(key,newNode);
            //同时,size加 1
            size++;
        }
    }


    //必须实现 Comparable 接口才可用于排序
    private class Node implements Comparable<Node>{
        int key;
        int value;
        int freq = 1;
        int index;

        public Node(){

        }

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

        @Override
        public int compareTo(Node o) {
            //优先比较频次 freq,频次相同再比较index
            int minus = this.freq - o.freq;
            return minus == 0? this.index - o.index : minus;
        }
    }
}

옵션 두 : 이중 연결리스트를 사용하여

아이디어 :

만 이중 연결리스트는 주파수와 시간 순서를 보호합니다. 그래서, 당신은 생각할 수 있습니다. 큰 주파수 앞에 넣어 작은 주파수는 주파수 되돌려. 주파수가 크고, 그것의 앞에 삽입되고보다 자주 제 소자까지 현재 노드로부터 이송 다시 동일한 경우. 마지막 측에서 최근 방문의 동일한 주파수 요소를 보장 할 수 있도록 (꼬리에 통과하면 물론, 꼬리는 전면에 삽입).

전반적으로,이 때문에, 낮은 주파수 및 상기 요소는 그리스트의 선두에 확실히 접속 부족을 가장 효율적이다. 캐시의 용량이 가득 찬 경우이 경우에는, 바로 머리 노드를 제거했다. 그러나, 우리는 머리와 꼬리 노드 꼬리를 대표하는 노드 머리에 두 개의 감시 노드와, 삽입과 삭제 작업의 편의를 위해 여기 나열합니다. 따라서 헤드 노드가 삭제 head.next에 해당 제거합니다.

PS : 단지 질량에 센티넬 노드는 더 이상 현재 노드의 위치를 ​​결정, 단순히 삽입 및 삭제 목록, 실제로 유효한 데이터가 저장되지 않습니다. 소자 단으로 현재 노드가 헤드 노드 위치 또는 테일 노드를 차지 그렇지 않으면, 노드는 재 요구 그것은 곤란이다.

새로운 노드가 목록은 다음지도에서 해당 위치에 삽입하는 방법의 이해를 용이하게하기 위해 :

다음과 같이 코드입니다 :

public class LFUCache {

    public static void main(String[] args) {
        LFUCache cache = new LFUCache(2);
        cache.put(1, 1);
        cache.put(2, 2);
        // 返回 1
        System.out.println(cache.get(1));
        cache.put(3, 3);    // 去除 key 2
        // 返回 -1 (未找到key 2)
        System.out.println(cache.get(2));
        // 返回 3
        System.out.println(cache.get(3));
        cache.put(4, 4);    // 去除 key 1
        // 返回 -1 (未找到 key 1)
        System.out.println(cache.get(1));
        // 返回 3
        System.out.println(cache.get(3));
        // 返回 4
        System.out.println(cache.get(4));

    }

    private Map<Integer,Node> cache;
    private Node head;
    private Node tail;
    private int capacity;
    private int size;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
        /**
         * 初始化头结点和尾结点,并作为哨兵节点
         */
        head = new Node();
        tail = new Node();
        head.next = tail;
        tail.pre = head;
    }

    public int get(int key) {
        Node node = cache.get(key);
        if(node == null) return -1;
        node.freq++;
        moveToPostion(node);
        return node.value;
    }

    public void put(int key, int value) {
        if(capacity == 0) return;
        Node node = cache.get(key);
        if(node != null){
            node.value = value;
            node.freq++;
            moveToPostion(node);
        }else{
            //如果元素满了
            if(size == capacity){
                //直接移除最前面的元素,因为这个节点就是频次最小,且最久未访问的节点
                cache.remove(head.next.key);
                removeNode(head.next);
                size--;
            }
            Node newNode = new Node(key, value);
            //把新元素添加进来
            addNode(newNode);
            cache.put(key,newNode);
            size++;
        }
    }

    //只要当前 node 的频次大于等于它后边的节点,就一直向后找,
    // 直到找到第一个比当前node频次大的节点,或者tail节点,然后插入到它前面
    private void moveToPostion(Node node){
        Node nextNode = node.next;
        //先把当前元素删除
        removeNode(node);
        //遍历到符合要求的节点
        while (node.freq >= nextNode.freq && nextNode != tail){
            nextNode = nextNode.next;
        }
        //把当前元素插入到nextNode前面
        node.pre = nextNode.pre;
        node.next = nextNode;
        nextNode.pre.next = node;
        nextNode.pre = node;

    }

    //添加元素(头插法),并移动到合适的位置
    private void addNode(Node node){
        node.pre = head;
        node.next = head.next;
        head.next.pre = node;
        head.next = node;
        moveToPostion(node);
    }

    //移除元素
    private void removeNode(Node node){
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    class Node {
        int key;
        int value;
        int freq = 1;
        //当前节点的前一个节点
        Node pre;
        //当前节点的后一个节点
        Node next;

        public Node(){

        }

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

당신은이 요소를 삽입하거나 요소를 제거 할 시간이 있는지 여부를 확인 할 수 있습니다, 당신은 전초 림프절의 장점을 설정하는 것입니다 추가 판단을 필요로하지 않습니다.

당신이 요소에 액세스 할 때마다이 일정한 규칙에 의해 요구되기 때문에 적절한 장소에 배치 요소, 따라서 앞뒤로에서 필수 요소가 통과되었습니다. 따라서, O (N)의 시간 복잡도.

옵션 세 : 그것은 LinkedHashSet과 유지 보수 주파수 목록

아이디어 :

주파수 및 액세스 시간을 유지하면서 우리는 더 이상 연결리스트를 사용하지 않습니다. 여기서, 전류 값으로서 주파수에 대응하는 구경 목록 순서를 갖는 키와 같은 주파수로 유지하는 키의지도로 대체. 다음과 같이 그 구조는 다음과 같습니다

Map<Integer, LinkedHashSet<Node>> freqMap

첫 번째 요소는 물론 현재 주파수로 반복하므로 반복자 반복적 방법 LinkedHashSet의 삽입 순서이기 때문에, 소자는 적어도 최근에 액세스. 이 경우, 상기 버퍼 캐시가 가득 찬 경우, 상기 제 1 엘리먼트 반복 삭제.

추가 freqMap에서, 또한이 관계를 유지하기 위해 다시 각 시간의 요소에 액세스해야합니다. 현재 요소의 주파수에 대응하는 이중 연결리스트에서 현재의 요소를 제거하고, 높은 주파수의 목록에 추가.

public class LFUCache1 {

    public static void main(String[] args) {
        LFUCache1 cache = new LFUCache1(2);
        cache.put(1, 1);
        cache.put(2, 2);
        // 返回 1
        System.out.println(cache.get(1));
        cache.put(3, 3);    // 去除 key 2
        // 返回 -1 (未找到key 2)
        System.out.println(cache.get(2));
        // 返回 3
        System.out.println(cache.get(3));
        cache.put(4, 4);    // 去除 key 1
        // 返回 -1 (未找到 key 1)
        System.out.println(cache.get(1));
        // 返回 3
        System.out.println(cache.get(3));
        // 返回 4
        System.out.println(cache.get(4));
    }

    //缓存 cache
    private Map<Integer,Node> cache;
    //存储频次和对应双向链表关系的map
    private Map<Integer, LinkedHashSet<Node>> freqMap;
    private int capacity;
    private int size;
    //存储最小频次值
    private int min;

    public LFUCache1(int capacity) {
        this.capacity = capacity;
        cache = new HashMap<>();
        freqMap = new HashMap<>();
    }

    public int get(int key) {
        Node node = cache.get(key);
        if(node == null) return -1;
        //若找到当前元素,则频次加1
        freqInc(node);
        return node.value;
    }

    public void put(int key, int value) {
        if(capacity == 0) return;
        Node node = cache.get(key);
        if(node != null){
            node.value = value;
            freqInc(node);
        }else{
            if(size == capacity){
                Node deadNode = removeNode();
                cache.remove(deadNode.key);
                size --;
            }
            Node newNode = new Node(key,value);
            cache.put(key,newNode);
            addNode(newNode);
            size++;
        }
    }

    //处理频次map
    private void freqInc(Node node){
        //从原来的频次对应的链表中删除当前node
        LinkedHashSet<Node> set = freqMap.get(node.freq);
        if(set != null)
            set.remove(node);
        //如果当前频次是最小频次,并且移除元素后,链表为空,则更新min值
        if(node.freq == min && set.size() == 0){
            min = node.freq + 1;
        }
        //添加到新的频次对应的链表
        node.freq ++;
        LinkedHashSet<Node> newSet = freqMap.get(node.freq);
        //如果高频次链表还未存在,则初始化一条
        if(newSet == null){
            newSet = new LinkedHashSet<Node>();
            freqMap.put(node.freq,newSet);
        }
        newSet.add(node);
    }

    //添加元素,更新频次
    private void addNode(Node node){
        //添加新元素,肯定是需要加入到频次为1的链表中的
        LinkedHashSet<Node> set = freqMap.get(1);
        if(set == null){
            set = new LinkedHashSet<>();
            freqMap.put(1,set);
        }
        set.add(node);
        //更新最小频次为1
        min = 1;
    }

    //删除频次最小,最久未访问的元素
    private Node removeNode(){
        //找到最小频次对应的 LinkedHashSet
        LinkedHashSet<Node> set = freqMap.get(min);
        //迭代到的第一个元素就是最久未访问的元素,移除之
        Node node = set.iterator().next();
        set.remove(node);
        //如果当前node的频次等于最小频次,并且移除元素之后,set为空,则 min 加1
        if(node.freq == min && set.size() == 0){
            min ++;
        }
        return node;
    }

    private class Node {
        int key;
        int value;
        int freq = 1;

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

        public Node(){

        }
    }
}

옵션 4 : 수동으로 주파수 목록을 달성

아이디어 :

의 JDK의 3 개 프로그램이 그래서 해시를 계산하는 W 연관된 줄이기 위해, 클래스 해시 테이블 및 양방향 연결리스트를 달성하는 것입니다 그것은 LinkedHashSet을 제공하기 때문에, 대신에 이중 연결리스트의 우리 자신의 실현, 효율성을 향상시킬 수 있습니다.

그래서,이 이중 연결리스트, 우리는 현재 주문이 주파수의 모든 요소에 액세스 할 수 있습니다 유지해야합니다. 우리는 첫 번째 보간 방법을 사용하여 목록의 앞에 추가 된 새로운 요소를 추가,이 경우,리스트의 마지막에 대한 접근의 부족에서 대부분의 요소입니다.

마찬가지로, 우리는 또한리스트의 작업을 용이하게하기 위해, 머리와 꼬리를 표현하기 위해 두 개의 노드와 노드를 감시.

다음과 같이 코드입니다 :

public class LFUCache2 {

    public static void main(String[] args) {
        LFUCache2 cache = new LFUCache2(2);
        cache.put(1, 1);
        cache.put(2, 2);
        // 返回 1
        System.out.println(cache.get(1));
        cache.put(3, 3);    // 去除 key 2
        // 返回 -1 (未找到key 2)
        System.out.println(cache.get(2));
        // 返回 3
        System.out.println(cache.get(3));
        cache.put(4, 4);    // 去除 key 1
        // 返回 -1 (未找到 key 1)
        System.out.println(cache.get(1));
        // 返回 3
        System.out.println(cache.get(3));
        // 返回 4
        System.out.println(cache.get(4));
    }

    private Map<Integer,Node> cache;
    private Map<Integer,DoubleLinkedList> freqMap;
    private int capacity;
    private int size;
    private int min;

    public LFUCache2(int capacity){
        this.capacity = capacity;
        cache = new HashMap<>();
        freqMap = new HashMap<>();
    }

    public int get(int key){
        Node node = cache.get(key);
        if(node == null) return -1;
        freqInc(node);
        return node.value;
    }

    public void put(int key, int value){
        if(capacity == 0) return;
        Node node = cache.get(key);
        if(node != null){
            node.value = value; //更新value值
            freqInc(node);
        }else{
            //若size达到最大值,则移除频次最小,最久未访问的元素
            if(size == capacity){
                //因链表是头插法,所以尾结点的前一个节点就是最久未访问的元素
                DoubleLinkedList list = freqMap.get(min);
                //需要移除的节点
                Node deadNode = list.tail.pre;
                cache.remove(deadNode.key);
                list.removeNode(deadNode);
                size--;
            }
            //新建一个node,并把node放到频次为 1 的 list 里面
            Node newNode = new Node(key,value);
            DoubleLinkedList newList = freqMap.get(1);
            if(newList == null){
                newList = new DoubleLinkedList();
                freqMap.put(1,newList);
            }
            newList.addNode(newNode);
            cache.put(key,newNode);
            size++;
            min = 1;//此时需要把min值重新设置为1
        }

    }

    //修改频次
    private void freqInc(Node node){
        //先删除node对应的频次list
        DoubleLinkedList list = freqMap.get(node.freq);
        if(list != null){
            list.removeNode(node);
        }
        //判断min是否等于当前node的频次,且当前频次的list为空,是的话更新min值
        if(min == node.freq && list.isEmpty()){
            min ++;
        }
        //然后把node频次加 1,并把它放到高频次list
        node.freq ++;
        DoubleLinkedList newList = freqMap.get(node.freq);
        if(newList == null){
            newList = new DoubleLinkedList();
            freqMap.put(node.freq, newList);
        }
        newList.addNode(node);
    }


    private class Node {
        int key;
        int value;
        int freq = 1;
        Node pre;
        Node next;

        public Node(){

        }

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

    //自实现的一个双向链表
    private class DoubleLinkedList {
        Node head;
        Node tail;

        // 设置两个哨兵节点,作为头、尾节点便于插入和删除操作
        public DoubleLinkedList(){
            head = new Node();
            tail = new Node();
            head.next = tail;
            tail.pre = head;
        }

        //采用头插法,每次都插入到链表的最前面,即 head 节点后边
        public void addNode(Node node){
            node.pre = head;
            node.next = head.next;
            //注意先把head的后节点的前节点设置为node
            head.next.pre = node;
            head.next = node;
        }

        //删除元素
        public void removeNode(Node node){
            node.pre.next = node.next;
            node.next.pre = node.pre;
        }

        //判断是否为空,即是否存在除了哨兵节点外的有效节点
        public boolean isEmpty(){
            //判断头结点的下一个节点是否是尾结点,是的话即为空
            return head.next == tail;
        }

    }

}

프로그램 오 : 양방향 중첩 된 목록

아이디어 :

반응식 III 및 IV에서 발견 될 수있는 프로그램은 주파수 사이의 관계를 저장하고 그 자체가 해시 테이블의 대응 링크를 freqmap하는데 사용된다. 이번에는 완전히 더욱 효율성을 개선하기 위해, freqMap을 대체하기 위해 자신의 양방향 연결리스트를 실현.

그러나, 구조, 그것은 이중 연결리스트는 각 요소는 이중 링크 목록을 다소 복잡하다. 이해를 용이하게하기 위해서, 그 구조는 I :( 편의상 플롯) 내부 체인 외부 체인이라는 다음과 같다

우리는 DoubleLinkedList로 구성된 이중 연결리스트로 전체를 넣어, 다음, 각 DoubleLinkedList 객체는 구성 노드의 이중 연결리스트입니다. 대부분의 HashMap의 형태 플러스 연결리스트의 배열처럼.

그러나, 우리가 배열이없는, 문제는 해시 충돌을 존재하지 않습니다. 그리고 이중, 목록을 연결 유연한 헤드 또는 동작 요소의 시작에서리스트의 꼬리를위한 센티넬가 존재한다.

여기서, firstLinkedList lastLinkedList 및 외부 노드는리스트의 머리와 꼬리를 나타냅니다. DoubleLinkedList 소자 레코드 링크리스트는 DoubleLinkedList2.freq 뒤에도. DoubleLinkedList1.freq보다 필드 주파수 주파수, 프런트 크고 작은 순서대로 연결리스트로 이루어지는 외층, 즉 더있다.

lastLinkedList.pre는 시간의 최소 주파수 내부 목록입니다 그래서에서 추가 DoubleLinkedList 필요의 새로운 주파수가 직접 전면 lastLinkedList 스카우트에 삽입있을 때마다.

내부 이중 연결리스트가 기지국에 의해 구성되고, 시작과 끝 감시 노드 및 제 1 보간 방법을 사용하여 두 가지 대표있다. 사실, 당신은 내부 목록 및 옵션 IV를 볼 수 있습니다, 그림과 같이 이중 연결리스트 구조는 동일 말할 것도 없다.

이 경우, 우리는, 최소 주파수이며 가장 최근에 액세스 할 수있는 요소를 찾을 수 있습니다

//频次最小,最久未访问的元素,cache满时需要删除
lastLinkedList.pre.tail.pre

따라서, 나는 코드를 이해하고자 :

public class LFUCache3 {

    public static void main(String[] args) {
        LFUCache3 cache = new LFUCache3(2);
        cache.put(1, 1);
        cache.put(2, 2);
        // 返回 1
        System.out.println(cache.get(1));
        cache.put(3, 3);    // 去除 key 2
        // 返回 -1 (未找到key 2)
        System.out.println(cache.get(2));
        // 返回 3
        System.out.println(cache.get(3));
        cache.put(4, 4);    // 去除 key 1
        // 返回 -1 (未找到 key 1)
        System.out.println(cache.get(1));
        // 返回 3
        System.out.println(cache.get(3));
        // 返回 4
        System.out.println(cache.get(4));
    }

    Map<Integer,Node> cache;
    /**
     * 这两个代表的是以 DoubleLinkedList 连接成的双向链表的头尾节点,
     * 且为哨兵节点。每个list中,又包含一个由 node 组成的一个双向链表。
     * 最外层双向链表中,freq 频次较大的 list 在前面,较小的 list 在后面
     */
    DoubleLinkedList firstLinkedList, lastLinkedList;
    int capacity;
    int size;

    public LFUCache3(int capacity){
        this.capacity = capacity;
        cache = new HashMap<>();
        //初始化外层链表的头尾节点,作为哨兵节点
        firstLinkedList = new DoubleLinkedList();
        lastLinkedList = new DoubleLinkedList();
        firstLinkedList.next = lastLinkedList;
        lastLinkedList.pre = firstLinkedList;
    }

    //存储具体键值对信息的node
    private class Node {
        int key;
        int value;
        int freq = 1;
        Node pre;
        Node next;
        DoubleLinkedList doubleLinkedList;

        public Node(){

        }

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

    public int get(int key){
        Node node = cache.get(key);
        if(node == null) return -1;
        freqInc(node);
        return node.value;
    }

    public void put(int key, int value){
        if(capacity == 0) return;
        Node node = cache.get(key);
        if(node != null){
            node.value = value;
            freqInc(node);
        }else{
            if(size == capacity){
                /**
                 * 如果满了,则需要把频次最小的,且最久未访问的节点删除
                 * 由于list组成的链表频次从前往后依次减小,故最小的频次list是 lastLinkedList.pre
                 * list中的双向node链表采用的是头插法,因此最久未访问的元素是 lastLinkedList.pre.tail.pre
                 */
                //最小频次list
                DoubleLinkedList list = lastLinkedList.pre;
                //最久未访问的元素,需要删除
                Node deadNode = list.tail.pre;
                cache.remove(deadNode.key);
                list.removeNode(deadNode);
                size--;
                //如果删除deadNode之后,此list中的双向链表空了,则删除此list
                if(list.isEmpty()){
                    removeDoubleLinkedList(list);
                }
            }
            //没有满,则新建一个node
            Node newNode = new Node(key, value);
            cache.put(key,newNode);
            //判断频次为1的list是否存在,不存在则新建
            DoubleLinkedList list = lastLinkedList.pre;
            if(list.freq != 1){
                DoubleLinkedList newList = new DoubleLinkedList(1);
                addDoubleLinkedList(newList,list);
                newList.addNode(newNode);
            }else{
                list.addNode(newNode);
            }
            size++;
        }
    }

    //修改频次
    private void freqInc(Node node){
        //从当前频次的list中移除当前 node
        DoubleLinkedList list = node.doubleLinkedList;
        if(list != null){
            list.removeNode(node);
        }
        //如果当前list中的双向node链表空,则删除此list
        if(list.isEmpty()){
            removeDoubleLinkedList(list);
        }
        //当前node频次加1
        node.freq++;
        //找到当前list前面的list,并把当前node加入进去
        DoubleLinkedList preList = list.pre;
        //如果前面的list不存在,则新建一个,并插入到由list组成的双向链表中
        //前list的频次不等于当前node频次,则说明不存在
        if(preList.freq != node.freq){
            DoubleLinkedList newList = new DoubleLinkedList(node.freq);
            addDoubleLinkedList(newList,preList);
            newList.addNode(node);
        }else{
            preList.addNode(node);
        }

    }

    //从外层双向链表中删除当前list节点
    public void removeDoubleLinkedList(DoubleLinkedList list){
        list.pre.next = list.next;
        list.next.pre = list.pre;
    }

    //知道了它的前节点,即可把新的list节点插入到其后面
    public void addDoubleLinkedList(DoubleLinkedList newList, DoubleLinkedList preList){
        newList.pre = preList;
        newList.next = preList.next;
        preList.next.pre = newList;
        preList.next = newList;
    }

    //维护一个双向DoubleLinkedList链表 + 双向Node链表的结构
    private class DoubleLinkedList {
        //当前list中的双向Node链表所有频次都相同
        int freq;
        //当前list中的双向Node链表的头结点
        Node head;
        //当前list中的双向Node链表的尾结点
        Node tail;
        //当前list的前一个list
        DoubleLinkedList pre;
        //当前list的后一个list
        DoubleLinkedList next;

        public DoubleLinkedList(){
            //初始化内部链表的头尾节点,并作为哨兵节点
            head = new Node();
            tail = new Node();
            head.next = tail;
            tail.pre = head;
        }

        public DoubleLinkedList(int freq){
            head = new Node();
            tail = new Node();
            head.next = tail;
            tail.pre = head;
            this.freq = freq;
        }

        //删除当前list中的某个node节点
        public void removeNode(Node node){
            node.pre.next = node.next;
            node.next.pre = node.pre;
        }

        //头插法将新的node插入到当前list,并在新node中记录当前list的引用
        public void addNode(Node node){
            node.pre = head;
            node.next = head.next;
            head.next.pre = node;
            head.next = node;
            node.doubleLinkedList = this;
        }

        //当前list中的双向node链表是否存在有效节点
        public boolean isEmpty(){
            //只有头尾哨兵节点,则说明为空
            return head.next == tail;
        }
    }


}

이 프로그램은 전체 삭제 조작의 목록을하기 때문에, 그래서 가능한 시간 복잡도 O (1).

발문

그리고 마지막으로 사실, 느낌은 코드가 비교적 쉽게 그것을 구현하는, 그것을 생각 합산. 그러나, 여전히 더 많은 연습을 작성해야합니다. 시간이 지남에 다시 ~ 봐

추천

출처www.cnblogs.com/starry-skys/p/12651282.html