【Leetcode】:LRU缓存机制引发的思考

1. 引言

题目:[LeetCode]146.LRU缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

进阶:

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

示例:

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

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 该操作会使得密钥 2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得密钥 1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

2. 解决方案分析

其实这道题并不难,就是找一个合适的数据结构去存储。

每次get之后,要把get到的数提前到最前面,如果没有get到,则返回-1。

put的时候,先查看有没有相同的key元素,有的话,直接把那个删掉,否则不做处理。然后判断当前的元素个数是否小于capacity,小于的话就在最前面添加新元素即可,否则在最前面添加新元素之后,还要把最后面的元素删掉。

3. 简单的实现方式

思路简单,难的是时间复杂度,我最开始直接想的是利用现成的数据结构,就用的是Java中的LinkedList和HashMap,HashMap中存储的是key和value,LinkedList中存储的是若干个map。代码如下:

package com.darrenchan.dp;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

class LRUCache2 {
    
    public int capacity;
    public List<Map<Integer, Integer>> list = new LinkedList<>();
    
    public LRUCache2(int capacity) {
        this.capacity = capacity;
    }
    
    /**
     * get方法的算法
     */
    public int get(int key) {
        int value = -1;
        for (Map<Integer, Integer> map : list) {
            if(map.get(key) != null){  
                value = map.get(key);
                list.remove(map);
                list.add(0, map);
                break;
            }
        }
        return value;
    }
    
    public void put(int key, int value) {
        int index = -1;
        for (Map<Integer, Integer> map : list) {
            if(map.get(key) != null){
                list.remove(map);
                break;
            }
        }
        int size = list.size();
        Map<Integer, Integer> map = new HashMap<>();
        map.put(key, value);
        if(size < capacity){
            list.add(0, map);
        }else{
            list.add(0, map);
            list.remove(capacity);
        }
    }
    
    public static void main(String[] args) {
        LRUCache2 lruCache = new LRUCache2(2);
        System.out.println(lruCache.get(2));
        lruCache.put(2, 6);
        System.out.println(lruCache.get(1));
        lruCache.put(1, 5);
        lruCache.put(1, 2);
        System.out.println(lruCache.get(1));
        System.out.println(lruCache.get(2));
    }
}

这样时间复杂度是O(N),因为每次需要for循环,时间超时。

3.优化的方案

Java自带的LinkedHashMap数据结构可以解决我们的问题,使get和put方法都在N(1)复杂度的情况下完成。先直接看代码吧。

class LRUCache {
    
    private int capacity;
    private LinkedHashMap<Integer,Integer> map;
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map =  new LinkedHashMap<>();
    }
    
  
    public int get(int key) {
      if(map.containsKey(key)){
          int value = map.get(key);
          map.remove(key);
          map.put(key,value);
          return value;
      }else{
          return -1;
      }
    }
    
    public void put(int key, int value) {
       if(map.containsKey(key)){
           map.remove(key);
       } 
       if(map.size() >= capacity){
           map.remove(map.keySet().iterator().next());
       }
      map.put(key,value);
    }
}

4. LinkedHashMap数据结构的说明

传统的HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。

HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。

这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序可以是插入顺序或者是访问顺序。

不过注意的是,该循环双向链表的头部存放的是最久访问的节点或最先插入的节点,尾部为最近访问的或最近插入的节点,迭代器遍历方向是从链表的头部开始到链表尾部结束,在链表尾部有一个空的header节点,该节点不存放key-value内容,为LinkedHashMap类的成员属性,循环双向链表的入口。

5. 总结

我们要熟练掌握JDK自带的数据结构的原理,这对提高我们的开发效率是非常有帮助的。

参考

  1. http://www.cnblogs.com/DarrenChan/p/8744354.html
  2. https://www.cnblogs.com/xiaoxi/p/6170590.html
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页