146. LRU 缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现
LRUCache 类:LRUCache(int capacity)以 正整数 作为容量capacity初始化 LRU 缓存int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1。void put(int key, int value)如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
示例:
输入 ["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 <= 30000 <= key <= 100000 <= value <= 105- 最多调用
2 * 105次get和put
解决办法
- 先搞懂什么是LRU
- 时间复杂度的要求
什么是LRU
一种页面缓存管理机制,类似图书馆的最近热门读书榜单,热门的书在前面,当有新的热门的书出现时,旧的相较与新书,不太热门的书就会淘汰出榜单
时间复杂度要求
当数据量n时,程序运行时间的“增长速度”有多快。 如果“增长速度”这个词还是有点抽象,你可以把它理解为“工作量的膨胀率”。
假设你是一个搬运工,老板给你安排了任务。而“增长速度”指的不是你搬一趟要多久,而是当任务量翻倍时,你累倒的速度有多快。
- O(1):老板让你去拿一个快递不管快递站里有 10 件快递还是 10,000 件快递,你手里有取件码,进去直接拿了就走。 增长速度: 零增长。即便快递站扩建成了物流园,你拿快递的体力消耗依然是一份。
- O(n):老板让你去数一排苹果,如果桌上有 10 个苹果,你数 10 下;如果桌上有 100 个苹果,你得数 100 下。 增长速度: 线性增长。苹果增加 10 倍,你的体力消耗就增加 10 倍。
用java解决LRU
1. 核心思路:寻找“完美的搭档”
设计LRU的难点在与在O(1)的时间复杂度内同时解决数据的 增、删、查 的操作,核心的思想就是借助高效的数据结构完成目标。且单一的数据结构完全不能满足要求(为什么?),所以我们需要结合两种数据结构完成要求
- hashmap 哈希表: 它的查找效率是O(1),但是它并不是有序的,且无法知道最近使用的时间
- Doubly Linked List 双向链表: 它的插入和删除节点是O(1) (前提是已经知道节点的位置).我们可以约定: 靠近头部的节点是最近使用的,靠近尾部的节点是最久没有使用的。(用这个数据结构来帮助管理)