LeetCode Notes_#146 LRU缓存机制

LeetCode Notes_#146 LRU缓存机制

Contents

题目


解答

方法1:哈希表

感觉这个题目挺有意思,我用HashMap实现了一个基本的解法,能通过除了一个大数据输入用例的其他所有用例,说明逻辑上是没有问题的。但是需要优化时间复杂度。

class LRUCache {
    HashMap<Integer, Integer> container;
    HashMap<Integer, Integer> noVisitTimes;
    int cap;
    int longestNoUseKey;

    public LRUCache(int capacity) {
        container = new HashMap<>();
        noVisitTimes = new HashMap<>();
        cap = capacity;
        longestNoUseKey = -1;
    }
    
    public int get(int key) {
        if(container.size() == 0) return -1;
        for(Integer k: container.keySet()){
            if(k == key) noVisitTimes.put(key, 0);
            else noVisitTimes.put(k, noVisitTimes.getOrDefault(k, 0) + 1);
        }
        if(container.containsKey(key)) return container.get(key);
        return -1; 
    }
    
    public void put(int key, int value) {
        for(Integer k: container.keySet()){
            if(k == key) noVisitTimes.put(key, 0);
            else noVisitTimes.put(k, noVisitTimes.getOrDefault(k, 0) + 1);
        }
        if(container.containsKey(key)){
            container.put(key, value);
        }else{
            if(container.size() < cap){
                container.put(key, value);
            }else{
                int maxNoUseTimes = -1;
                for(Integer k: noVisitTimes.keySet()){
                    if(noVisitTimes.get(k) > maxNoUseTimes){
                        maxNoUseTimes = noVisitTimes.get(k);
                        longestNoUseKey = k;
                    }
                }
                container.remove(longestNoUseKey);
                noVisitTimes.remove(longestNoUseKey);
                container.put(key, value);
            }
        }
    }
}

复杂度分析

时间复杂度:对于get()put()的操作,每次都需要遍历一遍大小为container.size()hashmap,复杂度为O(n),n为container.size()
空间复杂度:O(n),借助了一个额外的noVisitTimes的hashmap,大小和container相同

方法2:哈希表+双向链表

方法1使用哈希表存储<key, value>的键值对,这里修改为存储<key, DLinkedNode>的键值对。DLinkedNode是双向链表数据结构的节点。
问题的核心在于: 在缓存区满的情况下,要如何决定删除哪一个内容?当然是删除最旧的内容。
难点在于: 如何维护缓存内容的“新旧程度”,我们可以用双向链表来表达“新旧程度”,即新的内容在链表头部,旧的内容在链表尾部。
为什么一定要用双向链表? 主要是在删除内容时,需要删除链表当中的对应节点,如果用单向链表,删除一个节点时,需要给出这个节点的前序节点,比较麻烦。而双向链表的每个节点都是可以直接访问到它的前序节点的,这样删除起来会比较方便。

class LRUCache {
    //定义双向链表的节点数据结构
    class DLinkedNode{
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode(){}
        public DLinkedNode(int _key, int _value){
            key = _key;
            value = _value;
        }
    }
    //哈希表->查找缓存内容的“线头”
    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;
    //构造方法:初始化所有类变量,构造双向链表的头尾伪节点
    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
    //根据输入的key查找内容
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if(node == null) return -1;
        //如果key存在,先通过哈希表定位到双向链表的节点,再把节点移动到头部
        moveToHead(node);
        return node.value;
    }
    //新增输入的键值对
    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if(node == null){
            //如果key不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            //加入哈希表
            cache.put(key, newNode);
            //加入到双向链表的头部
            addToHead(newNode);
            ++size;
            if(size > capacity){
                //如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                //删除哈希表当中对应的项
                cache.remove(tail.key);
                --size;
            }
        }else{//如果key存在,先通过哈希表定位,再修改value,移动到头部
            node.value = value;
            moveToHead(node);
        }
    }
    //双向链表的一些方法
    //addToHead:put()新内容的时候,需要把新加入的键值对插入到双向链表的头部,表示这是最新的内容
    private void addToHead(DLinkedNode node){
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    //这里的removeNode()是removeTail()的工具方法
    private void removeNode(DLinkedNode node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    //put()时更新了一个原有的内容,这个内容变成了最新内容,需要移动到双向链表的头部;get()时查找了某个内容,这个内容也需要移动到双向链表的头部
    private void moveToHead(DLinkedNode node){
        removeNode(node);
        addToHead(node);
    }
    //removeNode():put()新内容后,如果超出了cache的capacity,就需要把最后一个节点(即最旧的内容)删除,
    private DLinkedNode removeTail(){
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

复杂度分析

时间复杂度:O(1)
空间复杂度:O(capacity)

posted @ 2021-01-25 20:42  Howfar's  阅读(68)  评论(0编辑  收藏  举报