Java每日一题——>剑指 Offer II 031. 最近最少使用缓存(分析合适的数据结构过程)

Posted stormzhuo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java每日一题——>剑指 Offer II 031. 最近最少使用缓存(分析合适的数据结构过程)相关的知识,希望对你有一定的参考价值。

这是LeetCode上的 [031,最近最少使用缓存],难度为 [中等]

题目

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

实现 LRUCache 类:

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

示例

输入
["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 <= 10000
  • 0 <= value <= 105
  • 最多调用 2 * 105 次 get 和 put

题解

题意要求get和put操作的时间复杂度都为O(1),因此哈希表符合要求,但哈希表无法找出最近最少使用的键,因此哈希表需要结合其他数据结构来满足题意,下面通过分析,什么样的数据结构满足最近最少使用的键

假设有一个数据结构的容器满足最近最少使用,它容量为n,初始为空。

当把添加元素以先后顺序添加到容器时,最先添加的元素在容器的头部,最后添加的元素在容器尾部,则容器的头部就是最近最少使用的元素

当执行put添加元素时,有两种情况。(忽略在容器判断是否有添加元素的时间,此操作需要结合哈希表)

若容器以存在添加元素,则put是更新元素(更新键的值),此时元素是最近最先使用的元素,需要做两个操作

  • 删除容器中原有的元素
  • 把元素添加到容器的尾部

若容器不存在添加元素,则put是添加元素,此时元素是最近最先使用的元素,只需要做一个操作,把元素添加到容器的尾部

综上分析,此容器需要满足在尾部添加元素和删除元素的时间复杂度都为O(1),很容易想到满足此条件的数据结构是双向链表

双向链表有头结点也有尾结点,即可以从头部遍历,也可以从尾部遍历,因此在尾部添加元素的时间复杂度为O(1)

双向链表每一个结点都有一个前置指针指向前一个结点,因此删除元素时,只需通过删除元素的前置指针找到删除元素的前一个结点即可完成删除元素,故时间复杂度为O(1)

前面在容器执行put时,我们忽略了在容器判断是否有添加元素的时间,例如在链表中添加元素,需要先判断添加元素是否在链表中,而此操作需要遍历链表,故时间复杂度为O(n),不满足题意,因此需要结合哈希表来帮我们判断添加元素是否在链表中。

哈希表的键存放添加元素的键,哈希表的值存放链表中的结点,当我们判断添加元素是否在链表时,只需通过哈希表即可判断,时间复杂度为O(1), get操作和put类似

代码实现

public class LRUCache 

    // 内部结点类
    private class ListNode 

        // 后置指针
        ListNode next;
        // 前置指针
        ListNode pre;
        // 结点存放键值对
        int key;
        int val;

        public ListNode(int key, int val) 
            this.key = key;
            this.val = val;
        
    

    /* 声明两个哨兵结点作为头结点和尾结点,减少逻辑判断
    * 例如当双向链表为空时,头结点和尾结点都为空,因此需要条件判断
    * 而用了哨兵结点后,头结点和尾结点都不为空,因此不需要条件判读*/
    private ListNode head;
    private ListNode tail;
    // 哈希表,键存放元素的键,值存放双向链表的结点
    private Map<Integer, ListNode> map;
    // LRU的容量
    private int capacity;

    public LRUCache(int capacity) 
        // 初始哈希表
        this.map = new HashMap();
        // 创建两个哨兵结点
        this.head = new ListNode(-1, -1);
        this.tail = new ListNode(-1, -1);
        // 初始时双向链表为空,则头结点后置指针指向尾结点
        head.next = tail;
        // 尾结点前置指针指向头结点
        tail.pre = head;
        this.capacity = capacity;
    

    public int get(int key) 
        // 通过哈希表获取链表中的结点
        ListNode node = map.get(key);
        // 若为null,则链表不存在此结点
        if (node == null) 
            return -1;
        
        /* 若不为null,则链表存在此结点,
        此结点就是最近最先使用,需要移动到链表的尾部,并删除此结点在链表原来的位置*/
        moveToTail(node, node.val);
        return node.val;
    

    public void put(int key, int val) 
        // 通过哈希表获取链表中的结点
        ListNode node = map.get(key);
        /* 若不等于null,此链表已有此结点,此时更新操作
        * 此结点是最近最先使用,故需要删除结点在链表原有的位置,并把结点移动到链表的尾部*/
        if (node != null) 
            moveToTail(node, val);
        // 若等于null,则是添加操作
         else 
            // 当LRU容量满时,需要先删除链表的第一个结点(最近最少使用),且需要删除哈希表记录的结点
            if (map.size() == capacity)
                // 获取链表第一个结点
                ListNode toBeDeleted = head.next;
                // 删除链表第一个结点
                deleteNode(toBeDeleted);
                // 删除哈希表记录的第一个结点
                map.remove(toBeDeleted.key);
            
            // 创建一个新结点
            ListNode newNode = new ListNode(key, val);
            // 新结点插入到链表的尾部
            insertToTail(newNode);
            // 在哈希表中记录插入的新结点
            map.put(key, newNode);
        
    

    private void moveToTail(ListNode node, int newVal) 
        // 删除结点在链表中原来的位置
        deleteNode(node);
        // 当put是更新操作时,需要更新结点的值
        node.val = newVal;
        // 把结点插入到链表的尾部
        insertToTail(node);
    

    private void insertToTail(ListNode newNode) 
        // 尾结点的前一个结点的后置指针指向新结点
        tail.pre.next = newNode;
        // 新结点的前置指针指向尾结点的前一个结点
        newNode.pre = tail.pre;
        // 新结点后置指针指向尾结点
        newNode.next = tail;
        // 尾结点的前置指针指向新结点
        tail.pre = newNode;
    

    private void deleteNode(ListNode node) 
        // 删除结点的前一个结点的后置指针指向删除结点的下一个结点
        node.pre.next = node.next;
        // 删除结点的下一个结点的前置指针指向删除结点的前一个结点
        node.next.pre = node.pre;
    


以上是关于Java每日一题——>剑指 Offer II 031. 最近最少使用缓存(分析合适的数据结构过程)的主要内容,如果未能解决你的问题,请参考以下文章

Java每日一题——>剑指 Offer II 035. 最小时间差(三解,蛮力,排序,哈希)

Java每日一题——> 剑指 Offer II 028. 展平多级双向链表

Java每日一题——>剑指 Offer II 027. 回文链表

Java每日一题——>剑指 Offer II 030. 插入删除和随机访问都是 O 的容器

Java每日一题——>剑指 Offer II 034. 外星语言是否排序

Java每日一题——>剑指 Offer II 034. 外星语言是否排序