我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。
-
如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
-
如果此数据没有在缓存链表中,又可以分为两种情况:
如果此时缓存未满,则将此结点直接插入到链表的头部;
如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
现在我们来看下缓存访问的时间复杂度是多少。因为不管缓存有没有满,我们都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)。
继续优化这个实现思路,使用一个哈希表和一个双向链表。
哈希表用于在O(1)的时间复杂度内查找数据,双向链表用于在O(1)的时间复杂度内添加、更新和删除数据。
举例子(基于Java实现):
import java.util.*;
public class LRUCache<K, V> {
private final int capacity; // 缓存的容量
private final Map<K, Node<K, V>> cache; // 缓存的哈希表
private final Node<K, V> head, tail; // 双向链表的头节点和尾节点
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>(capacity);
this.head = new Node<>(null, null);
this.tail = new Node<>(null, null);
head.next = tail;
tail.prev = head;
}
public V get(K key) {
Node<K, V> node = cache.get(key);
if (node == null) {
return null; // 如果节点不存在,返回null
}
moveToHead(node); // 将节点移动到双向链表的头部
return node.value;
}
public void put(K key, V value) {
Node<K, V> node = cache.get(key);
if (node != null) {
node.value = value; // 更新节点的值
moveToHead(node); // 将节点移动到双向链表的头部
} else {
node = new Node<>(key, value); // 创建新节点
cache.put(key, node); // 将新节点添加到哈希表
addToHead(node); // 将新节点添加到双向链表的头部
if (cache.size() > capacity) {
removeTail(); // 如果超过缓存的容量,删除双向链表的尾节点
}
}
}
private void moveToHead(Node<K, V> node) {
removeNode(node); // 删除节点
addToHead(node); // 将节点添加到双向链表的头部
}
private void addToHead(Node<K, V> node) {
node.next = head.next; // 更新节点的next指针
node.prev = head; // 更新节点的prev指针
head.next.prev = node; // 更新头节点的next指针的prev指针
head.next = node; // 更新头节点的next指针
}
private void removeNode(Node<K, V> node) {
node.prev.next = node.next; // 更新节点的prev指针的next指针
node.next.prev = node.prev; // 更新节点的next指针的prev指针
}
private void removeTail() {
Node<K, V> tailNode = tail.prev; // 获取双向链表的尾节点
removeNode(tailNode); // 删除尾节点
cache.remove(tailNode.key); // 从哈希表中删除尾节点
}
private static class Node<K, V> {
K key;
V value;
Node<K, V> prev, next; // 双向链表的prev指针和next指针
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "one");
cache.put(2, "two");
cache.put(3, "three");
System.out.println(cache.get(1)); // 输出: one
cache.put(4, "four"); // 淘汰键2
System.out.println(cache.get(2)); // 输出: null
}
}