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.
题述如上。
第一次编写的代码,思路是通过引用计数器来实现,功能通过,但又因为要不断地维护这个计数器而效率低下导致未accepted。
public class LRUCache { class Cache { int key; int value; int count; } Cache[] Cc; public LRUCache(int capacity) { Cc = new Cache[capacity]; for (int i = 0; i < Cc.length; i++) { Cc[i] = new Cache(); Cc[i].value = -1; Cc[i].count = i; } } public int get(int key) { for (int i = 0; i < Cc.length; i++) { if (Cc[i].key == key) { if (Cc[i].count != Cc.length - 1) { Cc[i].count = Cc.length - 1; for (int j = 0; j < Cc.length; j++) { Cc[j].count--; } } return Cc[i].value; } } return -1; } public void set(int key, int value) { for (int i = 0; i < Cc.length; i++) { Cc[i].count--; if (Cc[i].count == -1) { Cc[i].key = key; Cc[i].value = value; Cc[i].count = Cc.length - 1; } } } public static void main(String[] args) { LRUCache lru = new LRUCache(10); lru.set(2, 2); lru.set(3, 2); lru.set(11, 9); for (int i = 0; i < lru.Cc.length; i++) { System.out.println(lru.Cc[i].key + "count" + lru.Cc[i].count + "value" + lru.Cc[i].value); } System.out.println(lru.get(11)); } }
然后我又默默的优化了一下。思路还是一样的。但是少了很多多余的变量
class Map { int key; int value; } Map[] map; int capacity; public LRUCache(int capacity) { map = new Map[capacity]; for (int i = 0; i < capacity; i++) { map[i] = new Map(); } this.capacity = capacity; } public int get(int key) { int i = capacity - 1; int result = -1; Map temp = new Map(); while (i > 0) { if (map[i].key == key) { result = map[i].value; temp = map[i]; while (i < capacity - 1) { map[i] = map[i + 1]; i++; } map[capacity - 1] = temp; break; } i--; } return result; } public void set(int key, int value) { int i = 0; while (i < capacity - 1) { map[i].key = map[i + 1].key; map[i].value = map[i + 1].value; i++; } map[capacity - 1].key = key; map[capacity - 1].value = value; }
但是还是没有通过:
Time Limit Exceeded
然后就不淡然了。。主要的问题是如何维护一个管道 。而且还是可调配顺序的。免不了要去用循环向后压数据,那么问题就在于如何去‘压’这个数。
然后。。想到了。。指针。。然后是自然的链表,有key和value其实可以用hashmap.但是不太想用已经现有的东西。所以我决定写一个由简到繁的代码。
第一个:用java里边自带的linkedhashmap。简直就是完全为了本题而出的。
简直像cheating.
public class LRUCache { private int capacity; private Map<Integer, Integer> map; public LRUCache(int c) { this.capacity = c; this.map = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) { protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { return size() > capacity; } }; } public int get(int key) { if (!map.containsKey(key)) { return -1; } return map.get(key); } public void set(int key, int value) { map.put(key, value); } }
第二个就是自己写个链表。然后用hashmap的
class Node { int key; int value; Node next; Node pre; Node(int key, int value) { this.key = key; this.value = value; } } int capacity; HashMap<Integer, Node> map; Node head = new Node(-1, -1); Node tail; int size = 0; public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<Integer, LRUCache.Node>(capacity); } public int get(int key) { Node temp = map.get(key); if (temp == null) { return -1; } else { removeNode(temp); temp = new Node(key, temp.value); addNodetoTail(temp); map.put(key, temp); return temp.value; } } public void set(int key, int value) { Node temp = map.get(key); if (temp == null) { if (size == capacity) { removeHead(); } size++; } else { removeNode(temp); } temp = new Node(key, value); addNodetoTail(temp); map.put(key, temp); } private void removeHead() { // TODO Auto-generated method stub map.remove(head.key); if (size > 1) { head = head.next; head.pre = null; } else { head = null; } size--; } private void removeNode(Node temp) { // TODO Auto-generated method stub if (size > 1) { if (temp.pre != null) {// 证明是head结点 temp.pre.next = temp.next; } else { head = head.next; head.pre = null; } if (temp.next != null)// 证明tail结点 { temp.next.pre = temp.pre; } else { tail = tail.pre; tail.next = null; } } else { tail = null; } } private void addNodetoTail(Node node) { // TODO Auto-generated method stub if (tail == null || head == null) { head = node; tail = node; head.next = tail; tail.pre = head; head.pre = null; tail.next = null; } else { tail.next = node; node.pre = tail; node.next = null; tail = node; } }
可以看到自己写链表的话效率上会快那么 一点。主要是因为自己的功能单一。所以相当于精简了不必要的代码而提高了效率。
第三个比较有挑战性啦。那就是连hashmap都自己来实现一下
hashmap的结构特别像一个珠帘。同样下标的数据将会放在一串珠子上。
public class LRUCache { class Node { int key; int value; Node next; Node pre; Node(int key, int value) { this.key = key; this.value = value; } } class Map { int key; Node value; Map next; Map(int key, Node value) { this.key = key; this.value = value; } Map(int key, Node value, Map next) { this.key = key; this.value = value; this.next = next; } } Map[] mymap; int capacity = 0; Node head = new Node(-1, -1); Node tail; int size = 0; int Mapsize = 1; public LRUCache(int capacity) { this.capacity = capacity; while (Mapsize < capacity) { Mapsize = Mapsize << 1; } mymap = new Map[Mapsize]; } public int get(int key) { int index = key & Mapsize - 1; for (Map e = mymap[index]; e != null; e = e.next) { if (e.key == key) { removeNode(e.value); Node temp = new Node(key, e.value.value); addNodetoTail(temp); e.value = temp; return e.value.value; } } return -1; } public void set(int key, int value) { Node temp = new Node(key, value); int index = key & Mapsize - 1; Map e = mymap[index]; for (; e != null; e = e.next) { if (e.key == key) { removeNode(e.value); e.value = temp; addNodetoTail(temp); return; } } Map next = mymap[index]; mymap[index] = new Map(key, temp, next); if (size == capacity) { removeHead(); } size++; addNodetoTail(temp); } private void removeHead() { // TODO Auto-generated method stub int index = head.key & Mapsize - 1; Map prev = mymap[index]; Map e = prev; while (e != null) { Map next = e.next; if (e.value.key == head.key) { if (prev == e) mymap[index] = next; else prev.next = next; } prev = e; e = next; } if (size > 1) { head = head.next; head.pre = null; } else { head = null; } size--; } private void removeNode(Node temp) { // TODO Auto-generated method stub if (size > 1) { if (temp.pre != null) {// 证明是head结点 temp.pre.next = temp.next; } else { head = head.next; head.pre = null; } if (temp.next != null)// 证明tail结点 { temp.next.pre = temp.pre; } else { tail = tail.pre; tail.next = null; } } else { tail = null; } } private void addNodetoTail(Node node) { // TODO Auto-generated method stub if (tail == null) { head = node; tail = node; head.next = tail; tail.pre = head; head.pre = null; tail.next = null; } else { tail.next = node; node.pre = tail; node.next = null; tail = node; } } public static void main(String[] args) { LRUCache lru = new LRUCache(2); lru.set(1, 1); lru.set(2, 1); get(lru, 4); lru.set(4, 1); get(lru, 1); get(lru, 2); // // for (int i = 0; i < 4; i++) { // get(lru, i); // // } // // get(lru, 5); // // get(lru, 9); // lru.set(8, 4); // // get(lru, 7); // lru.set(8, 5); // get(lru, 3); // lru.set(80, 5); // lru.set(10, 5); // lru.set(11, 5); // lru.set(1123, 5); // lru.set(10, 5); // get(lru, 8); // get(lru, 10); // get(lru, 113); // get(lru, 11); System.out.println("---------------------------------------------"); while (lru.head != null) { System.out.println(lru.head.key + "value" + lru.head.value); lru.head = lru.head.next; } System.out.println("---------------------------------------------" + lru.size); while (lru.tail != null) { System.out.println(lru.tail.key + "value" + lru.tail.value); lru.tail = lru.tail.pre; } // [set(2,1),get(2),set(3,2),get(2),get(3)] // Output: [1,1,-1] // Expected: [1,-1,1] // set(2,1),set(1,1),get(2),set(4,1),get(1),get(2) // System.out.println(8 & 15); } private static void get(LRUCache lru, int key) { System.out.println(lru.get(key) + "key=" + key); } }
高度参考 Hashmap的源代码,然后进行部分重现,对于Java 8的hashmap有时间将继续实现,Java 8在进行hashmap的实现时,如果 hash碰撞非常多将自动转换成treemap
主要实现难点及技巧:
int capacity = 0; Node head = new Node(-1, -1); Node tail; int size = 0; int Mapsize = 1; public LRUCache(int capacity) { this.capacity = capacity; while (Mapsize < capacity) { Mapsize = Mapsize << 1; } mymap = new Map[Mapsize]; }
在初始化的时候找到大于设置容量的最小的2的n次方。
用途:用于之后各种的计算下标,如想找到第5个数时,5&Mapsize-1。比如说Mapsize此时是16,那么就是5&15=5,101&1111=101=5.很神奇的地方是当下标大于size时,比如说16&15将等于0,也就是达到了循环赋值的目的。
public void set(int key, int value) { Node temp = new Node(key, value); int index = key & Mapsize - 1; Map e = mymap[index]; for (; e != null; e = e.next) { if (e.key == key) { removeNode(e.value); e.value = temp; addNodetoTail(temp); return; } } Map next = mymap[index]; mymap[index] = new Map(key, temp, next); if (size == capacity) { removeHead(); } size++; addNodetoTail(temp); }
set方法也是调试了许久,苦于java不支持指针所以要在map里加一个构造函数, Map(int key, Node value, Map next),将新加入的结点到在链表头。
private void removeHead() { // TODO Auto-generated method stub int index = head.key & Mapsize - 1; Map prev = mymap[index]; Map e = prev; while (e != null) { Map next = e.next; if (e.value.key == head.key) { if (prev == e) mymap[index] = next; else prev.next = next; } prev = e; e = next; } if (size > 1) { head = head.next; head.pre = null; } else { head = null; } size--; }
删除头指针,关键代码在于
Map prev = mymap[index]; Map e = prev; while (e != null) { Map next = e.next; if (e.value.key == head.key) { if (prev == e) mymap[index] = next; else prev.next = next; } prev = e; e = next; }
两个变量prev 和next 分别用于存储要删除结点的前一个和后一个结点,当找到要删除的结点e时,只需要将prev=next即可。
if (prev == e)是用来判断第一次循环时也就是mymap[index]的本身是不是要删除的那个结点,是的话直接等于下一个就好。
这次的运行效率自然应比上一个高了。
为了再次优化将构造函数变为
while (Mapsize < capacity*2) { Mapsize = Mapsize << 1; }
以减少hash碰撞。