LRU 缓存淘汰算法

72 阅读2分钟

我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。

  1. 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。

  2. 如果此数据没有在缓存链表中,又可以分为两种情况:

    如果此时缓存未满,则将此结点直接插入到链表的头部;

    如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。

现在我们来看下缓存访问的时间复杂度是多少。因为不管缓存有没有满,我们都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 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
     }
 }
 ​