缓存是一种提高数据读取性能的技术,比如常见的CPU缓存,数据库缓存以及浏览器缓存。
缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?
缓存淘汰策略,常见的有三种,先进先出策略FIFO,最少使用策略LFU,最近最少使用策略LRULeast Recently Used)。
散列表+双向链表实现LRU算法
我们需要维护一个按照访问时间从大到小有序排列的链表结构。
因为缓存大小有限,当缓存空间不够需要淘汰一个数据时,我们直接将链表头部的节点删除。
缓存的主要操作
- 向缓存中添加一个数据
- 从缓存中删除一个数据
- 在缓存中查找一个数据
具体的数据结构
下面我们来具体来看看前面讲到缓存的三个操作
- 在缓存中查找一个数据
散列表中查找的时间复杂度接近O(1),所以通过散列表,我们可以很快的在缓存中查找到数据。当找到数据之后,还需要将它移到双向链表的尾部。 - 从缓存中删除一个数据
我们需要找到数据所在的节点,然后将节点删除。通过散列表,我们可以在O(1)的时间复杂度里找到要删除的节点,因为我们使用的是双向链表,可以通过前驱指针O(1)的时间复杂度获取到前驱节点,删除节点的时间复杂度为O(1)。 - 向缓存中添加一个数据
先看数据是否在缓存中,如果已经在缓存中,将其移到到双向链表的尾部;如果不在其中,需要看缓存有没有满,如果没有满,将数据直接添加到链表的尾部,如果缓存已满,则需要删除双向链表的头节点,再讲数据添加到链表的尾部。
具体的代码实现
package com.luochao.algo.cache;
import java.util.HashMap;
/**
* @author luoChao
* @create_date 2019-08-19 10:08
* @desc 我们需要维护一个按照访问时间从大到小有序排列的链表结构,
* 因为缓存大小有限,当需要淘汰一个数据时,我们直接将链表头部的节点删除
*/
public class LruCache {
private Node head;
private Node end;
// 缓存上限
private int cacheLimit;
// 哈希表
private HashMap<String, Node> hashMap;
public LruCache(int cacheLimit) {
this.cacheLimit = cacheLimit;
hashMap = new HashMap<>();
}
/**
* 从缓存中查找一个数据,然后将其移到双向链表的尾部
* @param key key
* @return value
*/
public String get(String key) {
Node node = hashMap.get(key);
if (node == null) {
return null;
}
refreshNode(node);
return node.value;
}
/**
* 从缓存中移除一个数据
* @param key key
*/
public void remove(String key) {
Node node = hashMap.get(key);
if (node != null) {
removeNode(node);
hashMap.remove(key);
}
}
/**
* 向缓存中添加一个数据
* 缓存中存在,更新缓存的值,将其移到双向链表的尾部
* 缓存中不存在,判断缓存是否已满
* 已满,将链表的头节点删除,将数据添加的链表尾部
* 未满,直接将数据插入到链表尾部
* @param key key
* @param value value
*/
public void put(String key,String value) {
Node node = hashMap.get(key);
if (node != null) {
node.value = value;
refreshNode(node);
} else {
if (hashMap.size() >= cacheLimit) {
String oldKey = removeNode(head);
hashMap.remove(oldKey);
}
node = new Node(key, value);
addNode(node);
hashMap.put(key, node);
}
}
/**
* 刷新节点位置
* @param node 节点
*/
private void refreshNode(Node node) {
// 尾节点不需要移动
if (node == end) {
return;
}
// 移除节点
removeNode(node);
// 重新插入到双向链表的尾部
addNode(node);
}
/**
* 双向链表尾部插入节点
* @param node
*/
private void addNode(Node node) {
if (end != null) {
end.next = node;
node.pre = end;
node.next = null;
}
end = node;
if (head == null) {
head = node;
}
}
/**
* 删除节点
* @param node 要删除的节点
*/
private String removeNode(Node node) {
if (node == head && node == end) { // 移除唯一的节点
head = null;
end = null;
} else if (node == end){ // 移除尾节点
end = end.pre;
end.next = null;
} else if (node == head) { //移除头节点
head = head.next;
head.pre = null;
} else { // 移除中间节点
node.pre.next = node.next;
node.next.pre = node.pre;
}
return node.key;
}
public void show () {
Node node = head;
while (node != null) {
System.out.print(node.key + " ");
node = node.next;
}
System.out.println();
}
class Node {
public Node(String key, String value) {
this.key = key;
this.value = value;
}
Node pre;
Node next;
String key;
String value;
}
public static void main(String[] args) {
LruCache lruCache = new LruCache(5);
lruCache.put("6","66");
lruCache.put("1","11");
lruCache.put("5","5");
lruCache.put("2","22");
lruCache.show();
lruCache.get("3");
lruCache.put("5","55");
lruCache.show();
lruCache.put("3","33");
lruCache.put("4","44");
lruCache.get("5");
lruCache.show();
}
}