zoukankan      html  css  js  c++  java
  • Medium | LeetCode 146. LRU 缓存机制 | HashMap+双向链表 | LinkedHasp

    146. LRU 缓存机制

    运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制

    实现 LRUCache 类:

    • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
    • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
    • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

    进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?

    示例:

    输入
    ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
    [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
    输出
    [null, null, null, 1, null, -1, null, -1, 3, 4]
    
    解释
    LRUCache lRUCache = new LRUCache(2);
    lRUCache.put(1, 1); // 缓存是 {1=1}
    lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
    lRUCache.get(1);    // 返回 1
    lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
    lRUCache.get(2);    // 返回 -1 (未找到)
    lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
    lRUCache.get(1);    // 返回 -1 (未找到)
    lRUCache.get(3);    // 返回 3
    lRUCache.get(4);    // 返回 4
    

    提示:

    • 1 <= capacity <= 3000
    • 0 <= key <= 3000
    • 0 <= value <= 104
    • 最多调用 3 * 104getput

    解题思路

    为了在O(1)时间内取到某个键的值, 则需要使用HaspMap保存节点。
    为了实现LRU, 可使用一个双向链表, 每次put节点时, 先用HaspMap判断是否在缓存命中。如果命中, 则需要将链表里的命中的节点移动到头部。如果没有命中, 则直接新建节点添加到尾部。如果容量满了, 需要删除节点。则直接删除尾节点, 在此之前需要在HashMap里使用remove(Object key)方法删除HashMap保存的键值对。

    class LRUCache {
    
        class LRUNode {
            private int key;
            private int value;
    
            private LRUNode prev;
            private LRUNode next;
    
            public LRUNode() {
            }
    
            public LRUNode(int key, int value) {
                this.key = key;
                this.value = value;
            }
    
            public int getKey() {
                return key;
            }
    
            public void setKey(int key) {
                this.key = key;
            }
    
            public int getValue() {
                return value;
            }
    
            public void setValue(int value) {
                this.value = value;
            }
    
            public LRUNode getPrev() {
                return prev;
            }
    
            public void setPrev(LRUNode prev) {
                this.prev = prev;
            }
    
            public LRUNode getNext() {
                return next;
            }
    
            public void setNext(LRUNode next) {
                this.next = next;
            }
        }
    
        // 用来保存 键 和 双向链表节点的映射
        private Map<Integer, LRUNode> map;
        // LRU缓存的最大容量
        private final int MAX_CAPACITY;
        // 当前LRU的容量
        private int curCapacity;
    
        // 头结点和尾节点, 均为哑节点, 不保存任何信息
        private final LRUNode head, tail;
    
        public LRUCache(int capacity) {
            MAX_CAPACITY = capacity;
            curCapacity = 0;
            map = new HashMap<>(capacity);
            head = new LRUNode();
            tail = new LRUNode();
            head.next = tail;
        }
    
        // 删除双向链表的某个特定节点
        private void unlink(LRUNode node) {
            node.getPrev().setNext(node.getNext());
            node.getNext().setPrev(node.getPrev());
        }
    
        // 将某个节点添加到头部
        private void addToHead(LRUNode node) {
            node.setNext(head.getNext());
            node.setPrev(head);
            head.getNext().setPrev(node);
            head.setNext(node);
        }
    
        // 删除双向链表尾部节点
        private void deleteTail() {
            LRUNode tailNode = tail.getPrev();
            unlink(tailNode);
            map.remove(tailNode.getKey());
        }
    
        // 取值
        public int get(int key) {
            // 先看缓存是否命中
            LRUNode node = map.get(key);
            if (node != null) {
                // 缓存命中
                // 从双向链表删除命中节点
                unlink(node);
                // 将此命中节点添加到头部
                addToHead(node);
                return node.getValue();
            } else {
                // 未命中节点, 直接返回-1
                return -1;
            }
        }
    
        public void put(int key, int value) {
            //先看是否命中节点
            LRUNode node = map.get(key);
            if (node != null) {
                // 命中缓存和直接从双向链表删除节点
                node.setValue(value);
                unlink(node);
                // 然后将链表移动到头部
                addToHead(node);
                return;
            }
            // 如果没有命中节点, 则需要新建节点
            LRUNode newNode = new LRUNode(key, value);
            map.put(key, newNode);
            // 然后将此新节点添加到头部
            addToHead(newNode);
            // 容量自增
            curCapacity++;
            // 如果当前容量超过了最大的允许容量
            if (curCapacity > MAX_CAPACITY) {
                // 则需要删除尾部节点
                deleteTail();
                curCapacity--;
            }
        }
    }
    

    方法二: LinkedHaspMap

    LinkHashMap里, 将Map的Entry节点, 使用双向链表的形式连接了起来。并且在LinkedHashMap里, 使用一个accessOrder属性来标识, 将Entry节点连接起来的规则。如果为false则按插入顺序存储元素,如果是true则按访问顺序存储元素。

    HaspMap里留下了如下一些钩子(HOOK)

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }
    

    这三个钩子在LinkedHashMap均实现了。

    所以可以通过集成LinkedHaspMap的方式, 来重新实现一个LinkedHashMap。

    class LRUCache extends LinkedHashMap<Integer, Integer>{
        private int capacity;
        
        public LRUCache(int capacity) {
            // acessOrder必须传参为true, 这样它会按照访问的顺序连接链表
            // acessOrder为true, afterNodeAccess钩子会将刚刚访问的元素放到尾部
            super(capacity, 0.75F, true);
            this.capacity = capacity;
        }
    
        public int get(int key) {
            return super.getOrDefault(key, -1);
        }
    
        public void put(int key, int value) {
            super.put(key, value);
        }
    
        // 重写了删除头部节点的条件(Linkedhashmap是把最新的元素放在尾节点的), 这个方法在afterNodeInsertion这个钩子里被调用。
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            return size() > capacity; 
        }
    }
    
  • 相关阅读:
    IntelliJ IDEA注册码
    linux中patch命令 -p 选项
    设备文件简介
    《算法导论》——矩阵
    《算法导论》——数论
    linux常用查看硬件设备信息命令(转载)
    netstat和telnet命令在Windows7中的用法(转载)
    c++容器使用总结(转载)
    离散数学——数论算法
    c语言中内存对齐问题
  • 原文地址:https://www.cnblogs.com/chenrj97/p/14329839.html
Copyright © 2011-2022 走看看