[LeetCode] LRU Cache

来源:互联网 发布:在线文件管理系统源码 编辑:程序博客网 时间:2024/04/29 21:31

Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and set.

get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
set(key, value) - Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.

实现LRU需要使用一个hash map和一个双向链表,map用于O(1)时间内查早指定元素是否在cache里,而双向链表则允许在O(1)时间内将一个指定节点移到表头和删除尾部节点。如果使用数组的话,那么把一个元素移到表头,或者在表头插入新元素,都会用O(N)时间。

我之前还想过用另一种方法,记录每个元素上次被使用(包括get和set)的时间戳,然后根据时间戳排序。不过这样排序的key就成了时间戳,跟hash map中指定的整形的key有冲突,而且插入的时间复杂度不会是O(1)了。

注意在链表里一定要同时存储key和value,不能够只存储value,因为在删除list尾部节点时,需要同时将这个键值对在map中删除,而删除map中的元素是需要对应的key的。


我一开始比较懒,第一次尝试用了现成的LinkedList类,虽然模拟正确,但是超时了。每次刷新链表的时候,我会将链表中的指定元素先删除,然后再加到表头。

private LinkedList<SimpleEntry<Integer, Integer>> entries = new LinkedList<SimpleEntry<Integer, Integer>>();private Map<Integer, SimpleEntry<Integer, Integer>> map = new HashMap<Integer, SimpleEntry<Integer, Integer>>();private int capacity = 0;public LRUCache(int capacity) {this.capacity = capacity;}public int get(int key) {if (map.containsKey(key)) {SimpleEntry<Integer, Integer> entry = map.get(key);entries.remove(entry);entries.addFirst(entry);return entry.getValue();} else {return -1;}}public void set(int key, int value) {if (map.containsKey(key)) {SimpleEntry<Integer, Integer> entry = map.get(key);entry.setValue(value);entries.remove(entry);entries.addFirst(entry);} else {if (entries.size() < capacity) {map.put(key, new SimpleEntry<Integer, Integer>(key, value));entries.addFirst(map.get(key));} else {// remove the last itemint lastKey = entries.getLast().getKey();map.remove(lastKey);entries.removeLast();// add the new itemmap.put(key, new SimpleEntry<Integer, Integer>(key, value));entries.addFirst(map.get(key));}}}

由于Java里的Map.Entry只是一个抽象的接口,不能构造实例,所以我这里用了SimpleEntry类。当然,使用2个整数代替也是可以的,不过这样节点里就不是存的键值对的地址了,更新时需要多次赋值。

超时的原因是因为我用remove方法在删除链表中一个指定元素的时候,这个时候程序会遍历真个链表直到找到该元素。显然,如果容量为N,那么最坏情况是会需要O(N)时间。

这个问题也是这题的难点所在,在将某一个指定元素移到表头时,只允许用常数时间。要做到这点,就必须的自己动手构造双向链表了。因为如果使用现成的LinkedList,每次的实际操作起始地点都必须是链表表头。如果自己构造链表元素,可以手动插入prev和next指针,然后提取出指定元素并插入到表头。代码如下:

class DoublyLinkedListNode {DoublyLinkedListNode prev;DoublyLinkedListNode next;int key;int val;DoublyLinkedListNode(int key, int val) {this.key = key;this.val = val;}}private Map<Integer, DoublyLinkedListNode> map = new HashMap<Integer, DoublyLinkedListNode>();private DoublyLinkedListNode head = null;private int capacity;// Detach the given node from the list.private void detach(DoublyLinkedListNode node) {// A corner case for detach.if (node == this.head)this.head = node.next;node.prev.next = node.next;node.next.prev = node.prev;}// Attach the given node to the beginning of the list.private void attach(DoublyLinkedListNode node) {if (this.head != null) {DoublyLinkedListNode last = this.head.prev;this.head.prev = node;node.next = this.head;last.next = node;node.prev = last;} else {node.next = node;node.prev = node;}this.head = node;}public LRUCache(int capacity) {assert (capacity > 0);this.capacity = capacity;}public int get(int key) {if (map.containsKey(key)) {// Refresh the list.DoublyLinkedListNode node = map.get(key);detach(node);attach(node);return node.val;} else {return -1;}}public void set(int key, int value) {if (!map.containsKey(key)) {// If the capacity is reached, remove the last node and its// corresponding key-value pair.if (map.size() == this.capacity) {DoublyLinkedListNode last = this.head.prev;detach(last);map.remove(last.key);}// Add a new node and its corresponding key-value pair.DoublyLinkedListNode newHead = new DoublyLinkedListNode(key, value);attach(newHead);map.put(key, newHead);} else {// Update the value.DoublyLinkedListNode newHead = map.get(key);newHead.val = value;// Refresh the list.detach(newHead);attach(newHead);}}

注意这里链表的刷新和删除某元素,我都是通过attach和detach方法进行,以给出的指定元素为起点来进行操作,只需要常数时间。


另外网上有人指出可以直接扩展Java的LinkedHashMap这个类库,代码可以非常精简。

详细的API在这里可以找到http://docs.oracle.com/javase/7/docs/api/java/util/LinkedHashMap.html

这个类在支持基本的hash map功能的同时,另外将所有的键值对用一个双向链表连接起来,连接的顺序可以是元素访问的顺序(AccessOrder),也可以是元素插入的顺序(InsertionOrder)。根据题意,这里可以设置AccessOrder为true,通过使用现有的removeEldestEntry方法,可以顺利将cache中最不常用的一项移除。

public class LRUCache<K, V> extends LinkedHashMap<K, V> {private int capacity;private static final long serialVersionUID = 1L;public LRUCache(int capacity) {super(capacity, 0.75f, true); // 'true' for accessOrder.this.capacity = capacity;}@Overridepublic boolean removeEldestEntry(Entry<K, V> entry) {return (size() > this.capacity);}}
使用了一个带参数的构造函数:public LinkedHashMap (int initialCapacity, float loadFactor, boolean accessOrder)
其中第一个参数表示初始容量,第二个参数表示加载因子,一般是0.75f。这两个参数值不是太重要,设成其它值一般关系也不大。注意这里的容量只是初始容量,跟其它常用容器一样,当需要更多容量的时候会自动扩展。

不过严重怀疑这种方法在面试的时候是否被允许使用。。。

BTW,这个题目让我联想到了老师上课讲的,LRU已经不再是最好的data replacement方案了,而是他研究出的ARU Cache,现在已经融入了最新的Linux内核。需要用hash map和两个栈实现。不知道是否也可以设计个StackHashMap类去搞搞。

原创粉丝点击