zoukankan      html  css  js  c++  java
  • 【算法刷题】LRU缓存模拟

    本文为个人解题思路整理,水平有限,有问题欢迎交流


    概览

    这题有两个解决方案,第二个的性能比第一个稍强,但是建议练习第一种方法,当然实际使用中性能优先

    难度:中等

    核心知识点:自定义链表 + map


    题目来源

    牛客:https://www.nowcoder.com/practice/e3769a5f49894d49b871c09cadd13a61

    力扣:https://leetcode-cn.com/problems/lru-cache-lcci/

    本文依照牛客的要求解答,力扣的解决方案也是一样的


    题目内容

    设计LRU缓存结构,其大小在构造时确定

    需求功能如下

    • set(key, value):将记录(key, value)插入该结构
    • get(key):返回key对应的value值

    额外要求如下

    • set和get方法的时间复杂度为O(1)
    • 某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的。
    • 当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的。

    操作模式

    • [1,x,y]:表示set(x,y),即存储[x,y],若结构的长度超出上限,则将最久远的移除
    • [2,x]:表示get(x),即获取x对应值,若不存在则返回-1

    样例

    数据源1

    [[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
    

    输出1

    [1,-1]
    

    数据源2

    [[1,1,1],[1,2,2],[1,3,3],[1,4,4],[2,4],[2,3],[2,2],[2,1],[1,5,5],[2,1],[2,2],[2,3],[2,4],[2,5]],3
    

    输出2

    [4,3,2,-1,-1,2,3,-1,5]
    

    方法1

    解题思路

    • 先整理需求

      • 需要以O(1)的复杂度进行增删改查
      • 容器内的数据需要根据活跃度排序,一旦数据被使用那么需要将其调整到队首
      • 当容器内数据超出上限的时候踢出最不活跃的一个
    • 思考

      • O(1)的增删改查,首先想要的是map,但显然仅仅使用map不够完成其他需求
      • 将某个数据插入队首,显然队列或者链表比较方便完成,但是队列不方便操作队列中间的元素,那么只能使用链表了
      • 需要对容器设置上限,第一想法是队列,用不了,第二想法是使用map检查数量,但是怎么确定谁最久远呢

      基本确定下来使用map+链表

    • 整理思路

      • 使用map保存数据的key,并通过map.size()检查数据数量
      • 使用链表存储数据,新数据放在队首,在数据被使用时也将其挪到队首,那么队尾的就是最不活跃的那个了
      • map的value保存对链表元素的映射,那么即可在O(1)的复杂度下完成查找到数据,且链表的增删改都是O(1)的复杂度

    解题方案

    • 定义一个节点类Node:保存四个数据:key,value,上个节点pre,下个节点next

    • 构造一个链表:头为节点head,尾为节点tail,当然初识两个都为null

    • 定义map:key为数据的key,value为key对应的节点

    • set(x,y)

      1. 检查x在map中是否存在,若存在则可确定其节点,将其从map和链表中一起删除
      2. 构造一个节点Node(x,y),将其添加到链表的头部,并将(x,node)存储到map中,这个节点即最活跃的节点
      3. 检查map的大小是否超出限制,若超出限制则直接获取链表的尾结点,即最不活跃的元素
        1. 尾节点的key即map中的key,即可以确定map中的目标
        2. 删除map中的该节点,直接remove掉即可
        3. 删除链表中的尾结点,将尾结点向前挪一位即可
    • get(x)

      1. 检查x在map中是否存在
        • 存在
          1. 通过map即可确定节点位置
          2. 保存节点的字段value作为结果(但先不return)
          3. 将节点从链表中移除
          4. 将节点重新添加至链表头部
        • 不存在:保存-1作为结果即可

    完整代码

    public class Lru {
        public static void main(String[] args) {
            // write your code here
            Lru lru = new Lru();
        }
    
        public Lru() {
            int[][] operators = new int[][]{
                    {1, 1, 1},
                    {1, 2, 2},
                    {1, 3, 3},
                    {1, 4, 4},
                    {2, 4},
                    {2, 3},
                    {2, 2},
                    {2, 1},
                    {1, 5, 5},
                    {2, 1},
                    {2, 2},
                    {2, 3},
                    {2, 4},
                    {2, 5}
            };
            int[] ans = LRU(operators, 3);
            for (int i : ans) {
                System.out.println(i);
            }
        }
    
        Node head, tail;//链表的头尾,用于充当队列
        Map<Integer, Node> map;//用于存储key与节点的映射
        int maxSize;//最大长度,全局变量
        List<Integer> ansList;//答案列表
    
        public int[] LRU(int[][] operators, int k) {
            maxSize = k;
            head = tail = null;
            map = new HashMap<>();
            ansList = new ArrayList<>();
            for (int i = 0; i < operators.length; i++) {
                if (operators[i][0] == 1) {
                    put(operators[i][1], operators[i][2]);
                } else if (operators[i][0] == 2) {
                    get(operators[i][1]);
                }
            }
            return ansList.stream().mapToInt(m -> m.intValue()).toArray();
    
        }
    
        void put(int key, int value) {
            Node newHead;
            if (map.containsKey(key)) {
                //包括该节点,则获取该节点,并从链表中删除
                newHead = map.get(key);
                remove(newHead);
            }
            //重建节点,并重新放置于链表头部
            newHead = new Node(key, value);
            map.put(key, newHead);
            //将最新的节点添加到队列头部
            push(newHead);
            //超出上限,则移除最后一位
            if (map.size() > maxSize) {
                pop();
            }
        }
    
        void get(int key) {
            if (map.containsKey(key)) {
                //包括该节点,则获取节点
                ansList.add(map.get(key).val);
                //移除节点,添加到头部
                remove(map.get(key));
                push(map.get(key));
            } else {
                //不包括节点,返回-1
                ansList.add(-1);
            }
        }
    
        void remove(Node node) {
            //将首位相连,那么自己将被孤立出去
            //这里要特殊处理一下pre或next为空的情况
            Node pre = node.pre;
            Node next = node.next;
            if (head == tail) {
                //若本就只有一个节点,则清空队列
                head = null;
                tail = null;
            } else {
                //若当前节点为首节点,则首节点指向next
                if (head != node) {
                    pre.next = next;
                } else {
                    head = next;
                }
                //若当前节点为尾结点,则尾结点指向pre
                if (tail != node) {
                    next.pre = pre;
                } else {
                    tail = pre;
                }
            }
        }
    
        void push(Node node) {
            //将自己添加到头部
            //特殊处理当前为空的情况
            if (head == null) {
                head = tail = node;
            } else {
                node.pre = null;
                node.next = head;
                head.pre = node;
                head = node;
            }
        }
    
        void pop() {
            //将最后一位从map中移除
            map.remove(tail.key);
            //tail向前挪一位,此时tail有可能变为null
            tail = tail.pre;
        }
    
        /**
         * 自定义节点,用于构造链表
         */
        class Node {
            int key, val;
            Node pre, next;
    
            public Node(int key, int val) {
                this.key = key;
                this.val = val;
            }
        }
    }
    

    image-20200917194118181

    性能

    image-20200917194209976

    记得提交代码时删除打印结果,会影响性能


    方法2

    解题思路

    • 需求如方法1一样,不再赘述

    • 思考

      • 需要O(1)完成增删改查,那么肯定是map啦
      • 需要对数据进行排序,那么可以用跟方法1同样的处理,即被删除使用的节点,再重新添加至头部
      • 但是map没有头尾?其实有的,LinkedHashMap,也是map的一种,但是可以保留顺序,最先放入在头部,最后放入的在尾部,使用他的迭代器iterator可以从头开始迭代
    • 整理思路

      • 使用LinkedHashMap存储数据
      • 如果数据变动,那么将数据删除掉,重新添加即可
      • 当数据超过上限的时候,将第一个数据,即最不活跃的数据删掉

      这样整理后,最活跃的将在最末尾,最不活跃的在最前面,O(1)的时间即可删除掉

    解题方案

    • 建立LinkedHashMap用于存储数据

    • set(x,y)

      1. 检查是否存在key为x的数据,若有则删除掉
      2. 添加数据(x,y)到末尾
      3. 检查数据是否超出上限,若超出上限则将第一个数据删除掉
    • get(x):检查是否存在key为x的数据

      • 若有,获取这个数据,并将其删除,再将其添加到哈希表中
      • 若没有,则返回-1

    完整代码

    public class Lru2 {
        public static void main(String[] args) {
            // write your code here
            Lru2 lru = new Lru2();
        }
    
        public Lru2() {
            int[][] operators = new int[][]{
                    {1, 1, 1},
                    {1, 2, 2},
                    {1, 3, 3},
                    {1, 4, 4},
                    {2, 4},
                    {2, 3},
                    {2, 2},
                    {2, 1},
                    {1, 5, 5},
                    {2, 1},
                    {2, 2},
                    {2, 3},
                    {2, 4},
                    {2, 5}
            };
            int[] ans = LRU(operators, 3);
    //        for (int i : ans) {
    //            System.out.println(i);
    //        }
        }
    
        LinkedHashMap<Integer, Integer> map;//用于存储key与节点的映射
        int maxSize;//最大长度,全局变量
        List<Integer> ansList;//答案列表
    
        public int[] LRU(int[][] operators, int k) {
            //初始化
            maxSize = k;
            map = new LinkedHashMap<>();
            ansList = new ArrayList<>();
            //开始执行业务
            for (int i = 0; i < operators.length; i++) {
                if (operators[i][0] == 1) {
                    put(operators[i][1], operators[i][2]);
                } else if (operators[i][0] == 2) {
                    get(operators[i][1]);
                }
            }
            //结果从list转为数组
            return ansList.stream().mapToInt(m -> m.intValue()).toArray();
        }
    
        void put(int key, int value) {
            if (map.containsKey(key)) {
                //包括该节点,则获取该节点,并从链表中删除
                map.remove(key);
            }
            //重建节点,并重新放置于链表头部
            map.put(key, value);
            //将最新的节点添加到队列头部
            //超出上限,则移除最后一位
            if (map.size() > maxSize) {
                map.remove(map.entrySet().iterator().next().getKey());
            }
        }
    
        void get(int key) {
            int value = -1;
            if (map.containsKey(key)) {
                //包括该节点,则获取节点
                value = map.get(key);
                //移除节点,添加到头部
                map.remove(key);
                map.put(key, value);
            }
            ansList.add(value);
        }
    }
    

    执行结果

    image-20200917204637014

    性能

    image-20200917204705618

    提交代码记得关闭打印哟


    比较

    • 方法1使用了传统的map和链表,两者互补,从而满足业务需求
    • 方法2直接使用LinkedHashMap即可满足所有需求
    • 方法2性能稍微由于方法1,但两者差距不大

    后记

    不难看出其实方法1应该才是出题人想要考察的,然而LinkedHashMap直接满足了所有需求。。。

    所以建议按照方法1学习和理解这题,但实际应用中当然性能优先,学操作是好事,秀操作可不是


    作者:Echo_Ye

    WX:Echo_YeZ

    Email :echo_yezi@qq.com

    个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

  • 相关阅读:
    PHP—字符串编码
    使用html模板
    创建html模板
    默认.htpl改为.htpl
    eclipse导入项目前面有感叹号
    eclipse点不出方法
    eclipse界面混乱
    面试题
    多线程
    瀑布流
  • 原文地址:https://www.cnblogs.com/silent-bug/p/13687595.html
Copyright © 2011-2022 走看看