【算法刷题】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 map;//用于存储key与节点的映射
    int maxSize;//最大长度,全局变量
    List 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 map;//用于存储key与节点的映射
    int maxSize;//最大长度,全局变量
    List 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 :[email protected]

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

你可能感兴趣的:(【算法刷题】LRU缓存模拟)