1. 引言
题目:[LeetCode]146.LRU缓存机制
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
进阶:
你是否可以在 O(1) 时间复杂度内完成这两种操作?
示例:
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
2. 解决方案分析
其实这道题并不难,就是找一个合适的数据结构去存储。
每次get之后,要把get到的数提前到最前面,如果没有get到,则返回-1。
put的时候,先查看有没有相同的key元素,有的话,直接把那个删掉,否则不做处理。然后判断当前的元素个数是否小于capacity,小于的话就在最前面添加新元素即可,否则在最前面添加新元素之后,还要把最后面的元素删掉。
3. 简单的实现方式
思路简单,难的是时间复杂度,我最开始直接想的是利用现成的数据结构,就用的是Java中的LinkedList和HashMap,HashMap中存储的是key和value,LinkedList中存储的是若干个map。代码如下:
package com.darrenchan.dp;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
class LRUCache2 {
public int capacity;
public List<Map<Integer, Integer>> list = new LinkedList<>();
public LRUCache2(int capacity) {
this.capacity = capacity;
}
/**
* get方法的算法
*/
public int get(int key) {
int value = -1;
for (Map<Integer, Integer> map : list) {
if(map.get(key) != null){
value = map.get(key);
list.remove(map);
list.add(0, map);
break;
}
}
return value;
}
public void put(int key, int value) {
int index = -1;
for (Map<Integer, Integer> map : list) {
if(map.get(key) != null){
list.remove(map);
break;
}
}
int size = list.size();
Map<Integer, Integer> map = new HashMap<>();
map.put(key, value);
if(size < capacity){
list.add(0, map);
}else{
list.add(0, map);
list.remove(capacity);
}
}
public static void main(String[] args) {
LRUCache2 lruCache = new LRUCache2(2);
System.out.println(lruCache.get(2));
lruCache.put(2, 6);
System.out.println(lruCache.get(1));
lruCache.put(1, 5);
lruCache.put(1, 2);
System.out.println(lruCache.get(1));
System.out.println(lruCache.get(2));
}
}
这样时间复杂度是O(N),因为每次需要for循环,时间超时。
3.优化的方案
Java自带的LinkedHashMap数据结构可以解决我们的问题,使get和put方法都在N(1)复杂度的情况下完成。先直接看代码吧。
class LRUCache {
private int capacity;
private LinkedHashMap<Integer,Integer> map;
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new LinkedHashMap<>();
}
public int get(int key) {
if(map.containsKey(key)){
int value = map.get(key);
map.remove(key);
map.put(key,value);
return value;
}else{
return -1;
}
}
public void put(int key, int value) {
if(map.containsKey(key)){
map.remove(key);
}
if(map.size() >= capacity){
map.remove(map.keySet().iterator().next());
}
map.put(key,value);
}
}
4. LinkedHashMap数据结构的说明
传统的HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。
HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。
这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序可以是插入顺序或者是访问顺序。
不过注意的是,该循环双向链表的头部存放的是最久访问的节点或最先插入的节点,尾部为最近访问的或最近插入的节点,迭代器遍历方向是从链表的头部开始到链表尾部结束,在链表尾部有一个空的header节点,该节点不存放key-value内容,为LinkedHashMap类的成员属性,循环双向链表的入口。
5. 总结
我们要熟练掌握JDK自带的数据结构的原理,这对提高我们的开发效率是非常有帮助的。
参考