【LRU】 (最近最少使用)

LRU (最近最少使用)



一、LRU是什么?

LRU(Least Recently Used)是一种常见的缓存淘汰策略,核心思想是 “淘汰最长时间未被使用的缓存数据”。它通过追踪数据的访问历史,确保频繁使用的内容保留在缓存中,从而最大化缓存的命中率。

二、实现

1.常规算法

代码如下(示例):

import java.util.HashMap;
import java.util.Map;
// 双向链表节点
class DLinkedNode {
    int key;
    int value;
    DLinkedNode prev;
    DLinkedNode next;
    
    public DLinkedNode() {}
    public DLinkedNode(int key, int value) {
        this.key = key;
        this.value = value;
    }
}
public class LRUCache {
    private Map<Integer, DLinkedNode> cache;
    private int capacity;
    private int size;
    private DLinkedNode head; 
    private DLinkedNode tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.cache = new HashMap<>();
        
        // 初始化双向链表
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
    
    // 获取缓存值
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 访问后移至链表头部
        moveToHead(node);
        return node.value;
    }
    // 添加/更新缓存
    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 新增节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            cache.put(key, newNode);
            addToHead(newNode);
            size++;
            
            // 检查容量
            if (size > capacity) {
                DLinkedNode removed = removeTail();
                cache.remove(removed.key);
                size--;
            }
        } else {
            // 更新节点值并移至头部
            node.value = value;
            moveToHead(node);
        }
    }
    
    // 将节点添加到链表头部
    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    
    // 从链表中移除节点
    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }
    
    private DLinkedNode removeTail() {
        DLinkedNode removed = tail.prev;
        removeNode(removed);
        return removed;
    }
}

2.双栈更替

利用栈的先进后出的特点,假设元素1,2,3依此放入栈中,那么1就在栈底,3就在栈顶,那么元素处于的位置越高,元素就越“新”,这个顺序模拟的是我们取用元素的最新时间,而不是该元素的过期(淘汰)的时间,就像在生活中,我们频繁用到的东西往往会放在明面上,而好久都用不到的东西就会放在很深的角落。

那么对于一个单向栈,我们该如何限制总元素数量以及进行元素淘汰呢,因为需要淘汰的元素都在栈底,所以引入第二个栈B,在淘汰时,将栈A元素依次取出,并依次加入栈B,这时原栈A的栈底元素变成了栈B的栈顶元素,这时依据栈的大小就可以直接进行淘汰了。

代码如下(示例):

import java.util.Stack;

class LRUCache {
    private Stack<Integer> stackKey;
    private Stack<Integer> stackValue;
    private int capacity;
    public LRUCache(int capacity) {
        this.stackKey = new Stack<>();
        this.stackValue = new Stack<>();
        this.capacity = capacity;
    }
    public int get(int key) {
        // 先查找key是否存在
        int index = stackKey.search(key);
        if (index == -1) {
            return -1;
        } else {
            // 为了移动元素到栈顶,需要先弹出上面的元素
            Stack<Integer> tempKey = new Stack<>();
            Stack<Integer> tempValue = new Stack<>();
            
            // 将目标元素上面的元素暂存到临时栈
            for (int i = 0; i < index - 1; i++) {
                tempKey.push(stackKey.pop());
                tempValue.push(stackValue.pop());
            }
            
            // 获取目标元素
            int targetKey = stackKey.pop();
            int targetValue = stackValue.pop();
            
            // 先将临时栈的元素放回原栈
            while (!tempKey.isEmpty()) {
                stackKey.push(tempKey.pop());
                stackValue.push(tempValue.pop());
            }
            
            // 再将目标元素压入栈顶
            stackKey.push(targetKey);
            stackValue.push(targetValue);
            
            return targetValue;
        }
    }
    public void put(int key, int value) {
        // 先检查key是否已存在
        int index = stackKey.search(key);
        if (index == -1) {
            // key不存在,直接添加到栈顶
            stackKey.push(key);
            stackValue.push(value);
            
            // 检查是否超过容量
            if (stackKey.size() > capacity) {
                // 移除栈底元素(最久未使用)
                // 为了移除栈底元素,需要先弹出所有元素
                Stack<Integer> tempKey = new Stack<>();
                Stack<Integer> tempValue = new Stack<>();
                
                // 将所有元素移到临时栈
                while (!stackKey.isEmpty()) {
                    tempKey.push(stackKey.pop());
                    tempValue.push(stackValue.pop());
                }
                
                // 移除临时栈的栈底元素(原栈的栈底)
                tempKey.pop();
                tempValue.pop();
                
                // 将剩余元素放回原栈
                while (!tempKey.isEmpty()) {
                    stackKey.push(tempKey.pop());
                    stackValue.push(tempValue.pop());
                }
            }
        } else {
            // key已存在,需要更新值并移到栈顶
            
            // 先将目标元素上面的元素暂存到临时栈
            Stack<Integer> tempKey = new Stack<>();
            Stack<Integer> tempValue = new Stack<>();
            
            for (int i = 0; i < index - 1; i++) {
                tempKey.push(stackKey.pop());
                tempValue.push(stackValue.pop());
            }
            
            // 移除目标元素
            stackKey.pop();
            int oldValue = stackValue.pop();
            
            // 先将临时栈的元素放回原栈
            while (!tempKey.isEmpty()) {
                stackKey.push(tempKey.pop());
                stackValue.push(tempValue.pop());
            }
            
            // 将新值添加到栈顶
            stackKey.push(key);
            stackValue.push(value);
        }
    }
}

总结

双栈的优点在于不需要使用复杂的数据结构就可以实现,并且在双栈更替的过程可以根据需求进行自定义淘汰标准,在追求低时间复杂度时可以考虑双向链表和哈希表的组合。

双栈的扩展==》多栈,双栈最大的问题在于,在进行淘汰时,每次都需要双栈元素交替,导致需要抛出所有元素,那么采用多栈是否可以优化这一点呢,我们可以依据实际需要来控制单个栈的大小,当元素过多时则新建栈来进行存储,而双栈交替则变成多栈依次交替,而需要淘汰最早的元素时,仅需要拿到栈号,然后简单交替即可,同时,当过期时间引入成为元素的存储依据时,也可以依据建栈时的栈号,来定位元素位置,而更替本身也只需交换栈号即可。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值