在我们内存或者cache中有两种替换算法来保证内存或者cache中都是“热点”的数据,一个是LRU和LFU。这里面们先介绍LRU,在这里的get和set操作的都是O(1),因为这两种速度都是很快的,不能要求O(n)。
【问题】
设计一种缓存结构,该结构在构造时确定大小,假设大小为K,并有两个功能:
set(key, value):将记录(key, value)插入该结构。
get(key):返回key对应的value值。
【要求】
1.set和get方法的时间复杂度为O(1)。
2.某个key的set或get操作一旦发生,认为这个key的记录成了最经常使用的。
3.当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的。
【举例】
假设缓存结构的实例是cache,大小为3,并依次发生如下行为:
1.cache.set("A", 1)。最经常使用的记录为("A", 1)。
2.cache.set("B", 2)。最经常使用的记录为("B", 2),("A", 1)变为最不经常的。
3.cache.set("C", 3)。最经常使用的记录为("C", 3),("A", 1)还是最不经常的。
4.cache.get("A")。最经常使用的记录为("A", 1),("B", 2)变为最不经常的。
5.cache.set("D", 4)。大小超过了3,所以移除此时最不经常使用的记录("B", 2),
加入记录("D", 4),并且为最经常使用的记录,然后("C", 3)变为最不经常使用的记录。
【题解】
储存数据用hash_map,因为hash表的增删该查的复杂度都为O(1)
本来使用频率使用队列存储,使用的放在头,不使用的自动向后排,最不经常使用的在队尾
但每次从队列中取出正在使用的数据至队列头部的复杂度为O(N),不满足条件。所以只能使用双向链表来进行存储,hash表中存放着每个数据节点的地址参数,故链表的中数据位置调整复杂度为O(1)。最经常使用的数据节点在链表的尾部,不经常使用的在链表的尾部,因为缓存数据时需要经常使用的,而链表的尾部插入更加方便。
【代码】
1.基础数据结构Node(节点)
public static class Node<K, V> {
public K key;
public V value;
public Node<K, V> last;
public Node<K, V> next;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
}
2.基础数据结构NodeDoubleLinkedList(双向链表)
addNode方法表示在双向链表中从尾部添加一个新的节点。
moveNodeToTail方法使用前必须保证传入的Node节点在双向链表中,该方法表示把双向链表中的节点放在链表最后的操作。
removeHead方法表示删除双向链表的头结点。
public static class NodeDoubleLinkedList<K, V> {
private Node<K, V> head;
private Node<K, V> tail;
public NodeDoubleLinkedList() {
head = null;
tail = null;
}
public void addNode(Node<K, V> newNode) {
if (newNode == null) {
return;
}
if (head == null) {
head = newNode;
tail = newNode;
} else {
tail.next = newNode;
newNode.last = tail;
tail = newNode;
}
}
public void moveNodeToTail(Node<K, V> node) {
if (this.tail == node) {
return;
}
if (this.head == node) {
this.head = node.next;
this.head.last = null;
} else {
node.last.next = node.next;
node.next.last = node.last;
}
node.last = this.tail;
node.next = null;
this.tail.next = node;
this.tail = node;
}
public Node<K, V> removeHead() {
if (this.head == null) {
return null;
}
Node<K, V> res = this.head;
if (this.head == this.tail) {
this.head = null;
this.tail = null;
} else {
this.head = res.next;
res.next = null;
this.head.last = null;
}
return res;
}
}
3.基础数据结构MyCache(自定义缓存)
MyCache构造方法,自定义的缓存大小是固定不变的。定义一个HashMap结构keyNodeMap
用于查看K是否之前就有,V表示Node节点。再定义一个双向链表nodeList。
get方法表示如果HashMap结构中之前存在这个K,就把这个K所对应的节点在双向链表中的位置放在最后。
removeMostUnusedCache方法表示移除双向链表的头节点以及HashMap中的对应关系。
set方法表示如果之前的HashMap结构存储过这个对应关系,就修改这个节点的值,并且把这个节点在双向链表中的位置放在最后。如果之前的HashMap结构没有存储过这个对应关系,并且当前缓存容量未超过设定的值,则新建Node节点并且在HashMap结构和双向链表中添加对应关系。缓存容量超过设定值则使用 removeMostUnusedCache方法删除双向链表的头节点。
public static class MyCache<K, V> {
private HashMap<K, Node<K, V>> keyNodeMap;
private NodeDoubleLinkedList<K, V> nodeList;
private final int capacity;
public MyCache(int cap) {
if (cap < 1) {
throw new RuntimeException("should be more than 0.");
}
keyNodeMap = new HashMap<K, Node<K, V>>();
nodeList = new NodeDoubleLinkedList<K, V>();
capacity = cap;
}
public V get(K key) {
if (keyNodeMap.containsKey(key)) {
Node<K, V> res = keyNodeMap.get(key);
nodeList.moveNodeToTail(res);
return res.value;
}
return null;
}
public void set(K key, V value) {
if (keyNodeMap.containsKey(key)) {
Node<K, V> node = keyNodeMap.get(key);
node.value = value;
nodeList.moveNodeToTail(node);
} else {
if (keyNodeMap.size() == capacity) {
removeMostUnusedCache();
}
Node<K, V> newNode = new Node<K, V>(key, value);
keyNodeMap.put(key, newNode);
nodeList.addNode(newNode);
}
}
private void removeMostUnusedCache() {
Node<K, V> removeNode = nodeList.removeHead();
keyNodeMap.remove(removeNode.key);
}
}