加载中...
面试题 16.25-LRU 缓存(LRU Cache LCCI)
发表于:2021-12-03 | 分类: 中等
字数统计: 1.7k | 阅读时长: 8分钟 | 阅读量:

原文链接: https://leetcode-cn.com/problems/lru-cache-lcci

英文原文

Design and build a "least recently used" cache, which evicts the least recently used item. The cache should map from keys to values (allowing you to insert and retrieve a value associ­ated with a particular key) and be initialized with a max size. When it is full, it should evict the least recently used item.

You should implement following operations:  get and put.

Get a value by key: get(key) - If key is in the cache, return the value, otherwise return -1.
Write a key-value pair to the cache: put(key, value) - If the key is not in the cache, then write its value to the cache. Evict the least recently used item before writing if necessary.

Example:

LRUCache cache = new LRUCache( 2 /* capacity */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // returns 1
cache.put(3, 3);    // evicts key 2
cache.get(2);       // returns -1 (not found)
cache.put(4, 4);    // evicts key 1
cache.get(1);       // returns -1 (not found)
cache.get(3);       // returns 3
cache.get(4);       // returns 4

中文题目

设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

它应该支持以下操作: 获取数据 get 和 写入数据 put

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

示例:

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

通过代码

高赞题解

解题思路

$LRU$ 总体上是这样的,最近使用的放在前边(最左边),最近没用的放到后边(最右边),来了一个新的数,如果内存满了,把旧的数淘汰掉,那位了方便移动数据,我们肯定不能考虑用数组,呼之欲出,就是使用链表了,解决方案:链表(处理新老关系)+ 哈希(查询在不在),分析如下

  1. 底层应该用链表,按照数据的新旧程度来排列,旧的在左边,新的在右边,新来一个加到尾部(你可以想象自己从左往右画一条链表),删除是删头,除了这两个操作,还有就是把一个数据从中间拿出来放尾巴上(这个数组就很难做到)

  2. 这里还有一个需求,就是要知道这个数据有没有存在于链表中,如果不在链表中,加到尾巴即可,如果已经在链表中,就只要更细数据的位置,如何查找这个数据在不在呢,这就用哈希表。

  3. 考虑删除操作,要把当前节点的前一个节点的指针的改变,获取它前一个节点,方便的数据结构就是 双向链表

所以我们用的数据结构就是 $LinkedList$ (底层是双向链表)+ $HashMap$,也直接用 $LinkedHashMap$ 更为方便。看面试官要求是啥了。

ps:其实也可以用单链表,只要在 $map$ 中不存当前节点,而是存当前节点的前驱即可。

下面把三种方式都写一下

代码

解法一:使用 $LinkedHashMap$
你当然可以直接重写 $removeEldestEntry$ 方法,这里暂忽略此写法

[]
public class LRUCache{ int capacity; Map<Integer, Integer> map; public LRUCache(int capacity) { this.capacity = capacity; map = new LinkedHashMap<>(); } public int get(int key) { if (!map.containsKey(key)) { return -1; } // 先删除旧的位置,再放入新位置 Integer value = map.remove(key); map.put(key, value); return value; } public void put(int key, int value) { if (map.containsKey(key)) { map.remove(key); map.put(key, value); return; } map.put(key, value); // 超出capacity,删除最久没用的,利用迭代器删除第一个 if (map.size() > capacity) { map.remove(map.entrySet().iterator().next().getKey()); } } }

解法二:使用双链表+HashMap

[]
public class LRUCache{ private int capacity; private Map<Integer, ListNode> map; //key->node private ListNode head; // dummy head private ListNode tail; // dummy tail public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(); head = new ListNode(-1, -1); tail = new ListNode(-1, -1); head.next = tail; tail.pre = head; } public int get(int key) { if (!map.containsKey(key)) { return -1; } ListNode node = map.get(key); // 先删除该节点,再接到尾部 node.pre.next = node.next; node.next.pre = node.pre; moveToTail(node); return node.val; } public void put(int key, int value) { // 直接调用这边的get方法,如果存在,它会在get内部被移动到尾巴,不用再移动一遍,直接修改值即可 if (get(key) != -1) { map.get(key).val = value; return; } // 若不存在,new一个出来,如果超出容量,把头去掉 ListNode node = new ListNode(key, value); map.put(key, node); moveToTail(node); if (map.size() > capacity) { map.remove(head.next.key); head.next = head.next.next; head.next.pre = head; } } // 把节点移动到尾巴 private void moveToTail(ListNode node) { node.pre = tail.pre; tail.pre = node; node.pre.next = node; node.next = tail; } // 定义双向链表节点 private class ListNode { int key; int val; ListNode pre; ListNode next; public ListNode(int key, int val) { this.key = key; this.val = val; pre = null; next = null; } } }

解法三:使用单链表

[]
public class LRUCache{ private int capacity; private Map<Integer, ListNode> map; //key -> node.pre private ListNode head; // dummy private ListNode tail; public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(); head = new ListNode(-1, -1); tail = head; } public int get(int key) { if (!map.containsKey(key)) { return -1; } // map中存放的是要找的节点的前驱 ListNode pre = map.get(key); ListNode cur = pre.next; // 把当前节点删掉并移到尾部 if (cur != tail) { pre.next = cur.next; // 更新它后面 node 的前驱 map.put(cur.next.key, pre); map.put(cur.key, tail); moveToTail(cur); } return cur.val; } public void put(int key, int value) { if (get(key) != -1) { map.get(key).next.val = value; return; } // 若不存在则 new 一个 ListNode node = new ListNode(key, value); // 当前 node 的 pre 是 tail map.put(key, tail); moveToTail(node); if (map.size() > capacity) { map.remove(head.next.key); map.put(head.next.next.key, head); head.next = head.next.next; } } private void moveToTail(ListNode node) { node.next = null; tail.next = node; tail = tail.next; } // 定义单链表节点 private class ListNode { int key, val; ListNode next; public ListNode(int key, int val) { this.key = key; this.val = val; this.next = null; } } }

统计信息

通过次数 提交次数 AC比率
33860 62068 54.6%

提交历史

提交时间 提交结果 执行时间 内存消耗 语言
上一篇:
面试题 17.01-不用加号的加法(Add Without Plus LCCI)
下一篇:
面试题 16.26-计算器(Calculator LCCI)
本文目录
本文目录