[LeetCode] LRU Cache

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 item
				int lastKey = entries.getLast().getKey();
				map.remove(lastKey);
				entries.removeLast();
				// add the new item
				map.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;
	}

	@Override
	public 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类去搞搞。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值