14.1 LRU/LFU缓存实现

面试重要程度:⭐⭐⭐⭐⭐

常见提问方式: "设计LRU缓存" "实现LFU算法" "缓存淘汰策略对比"

预计阅读时间:40分钟

🎯 LRU缓存实现

LRU基本概念

LRU (Least Recently Used) 最近最少使用算法,是一种常用的缓存淘汰策略。

核心思想: 当缓存满时,优先淘汰最久未被访问的数据。

LRU算法实现

/**
 * LRU缓存实现
 * LeetCode 146: LRU Cache
 */
public class LRUCache {
    
    /**
     * 双向链表节点
     */
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        
        public DLinkedNode() {}
        
        public DLinkedNode(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    
    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;
    
    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        
        head.next = tail;
        tail.prev = head;
    }
    
    /**
     * 获取缓存值
     * 时间复杂度:O(1)
     */
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        
        // 如果key存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        
        return node.value;
    }
    
    /**
     * 添加缓存值
     * 时间复杂度:O(1)
     */
    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 tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        } else {
            // 如果key存在,先通过哈希表定位,再修改value,并移到头部
            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 lastNode = tail.prev;
        removeNode(lastNode);
        return lastNode;
    }
}

基于LinkedHashMap的简化实现

/**
 * 基于LinkedHashMap的LRU实现
 */
public class LRUCacheSimple extends LinkedHashMap<Integer, Integer> {
    private int capacity;
    
    public LRUCacheSimple(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        super.put(key, value);
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}

🔄 LFU缓存实现

LFU基本概念

LFU (Least Frequently Used) 最不经常使用算法,根据数据的历史访问频率来淘汰数据。

核心思想: 当缓存满时,优先淘汰访问频率最低的数据。

LFU算法实现

/**
 * LFU缓存实现
 * LeetCode 460: LFU Cache
 */
public class LFUCache {
    
    /**
     * 缓存节点
     */
    class Node {
        int key, val, freq;
        Node prev, next;
        
        public Node(int key, int val) {
            this.key = key;
            this.val = val;
            this.freq = 1;
        }
    }
    
    /**
     * 双向链表
     */
    class DoublyLinkedList {
        Node head, tail;
        
        public DoublyLinkedList() {
            head = new Node(0, 0);
            tail = new Node(0, 0);
            head.next = tail;
            tail.prev = head;
        }
        
        public void addFirst(Node node) {
            Node next = head.next;
            node.next = next;
            next.prev = node;
            node.prev = head;
            head.next = node;
        }
        
        public void remove(Node node) {
            Node prev = node.prev;
            Node next = node.next;
            prev.next = next;
            next.prev = prev;
        }
        
        public Node removeLast() {
            if (head.next == tail) {
                return null;
            }
            Node last = tail.prev;
            remove(last);
            return last;
        }
        
        public boolean isEmpty() {
            return head.next == tail;
        }
    }
    
    private int capacity;
    private int minFreq;
    private Map<Integer, Node> keyToNode;
    private Map<Integer, DoublyLinkedList> freqToList;
    
    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.minFreq = 0;
        this.keyToNode = new HashMap<>();
        this.freqToList = new HashMap<>();
    }
    
    public int get(int key) {
        Node node = keyToNode.get(key);
        if (node == null) {
            return -1;
        }
        
        // 增加频率
        increaseFreq(node);
        return node.val;
    }
    
    public void put(int key, int value) {
        if (capacity <= 0) return;
        
        Node node = keyToNode.get(key);
        if (node != null) {
            // 更新已存在的key
            node.val = value;
            increaseFreq(node);
            return;
        }
        
        // 添加新key
        if (keyToNode.size() >= capacity) {
            // 删除最少使用的节点
            removeMinFreqNode();
        }
        
        // 插入新节点
        Node newNode = new Node(key, value);
        keyToNode.put(key, newNode);
        freqToList.computeIfAbsent(1, k -> new DoublyLinkedList()).addFirst(newNode);
        minFreq = 1;
    }
    
    private void increaseFreq(Node node) {
        int freq = node.freq;
        
        // 从原频率链表中删除
        freqToList.get(freq).remove(node);
        
        // 如果原频率链表为空且是最小频率,更新最小频率
        if (freqToList.get(freq).isEmpty() && freq == minFreq) {
            minFreq++;
        }
        
        // 增加频率并添加到新频率链表
        node.freq++;
        freqToList.computeIfAbsent(node.freq, k -> new DoublyLinkedList()).addFirst(node);
    }
    
    private void removeMinFreqNode() {
        DoublyLinkedList list = freqToList.get(minFreq);
        Node node = list.removeLast();
        keyToNode.remove(node.key);
  

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Java面试圣经 文章被收录于专栏

Java面试圣经,带你练透java圣经

全部评论
求内推码
点赞 回复 分享
发布于 09-06 08:18 浙江

相关推荐

08-16 22:28
宁波大学 Java
put添加元素的流程1&nbsp;首先会去借助哈希值计算桶索引的值,运算函数为(n-1)&amp;hash值进行与计算。:计算哈希值,jdk7之前是直接引用哈希值计算,而jdk8开始则借助哈希扰动的算法,原理呢就是将原哈希值向右移动16位,异或运算哈希值,将高位哈希值与地位哈希值都可以很好的参与到计算当中,减少哈希冲突的概率2&nbsp;判断该桶索引位置是否为空,如果为空直接进行存放Node节点。如果不为空,需要遍历链表或者红黑树,去判断是否存在相同的key,如果不同则插入,相同则覆盖。:8开始为尾插,8之前为头插(多线程扩容可能会导致链表出现死循环的问题)插入新节点后3对数组的元素进行计数,当数组当中的元素数量大于负载因子与容量的乘积时,会触发扩容机制,两倍的扩容速度,扩容过程当中存在对元素桶索引的重新分配问题:在jdk7之前会使用(2n-1)&amp;hash重新算一遍桶索引的位置(n为原数组长度):但是在jdk8开始,将(2n-1)&amp;hash进行拆分,拆成(n-1)&amp;hash+n&amp;hash=原索引位置+n&amp;hash,在判断过程当中呢,实现对n&amp;hash的计算即可,判断计算是否为零,为零则保留原索引,不为零则在原索引的基础之上加上旧数组长度,接着移动就简单了,将原先的链表拆分为两个临时链表,后续直接一次性挂载即可。4判断是否需要树化,先判断链表长度,在链表长度达到8的条件下,判断数组长度是否达到64,达到就将链表树化,没达到64就以2倍的速度进行扩容。
如果再来一次,你还会选择...
点赞 评论 收藏
分享
09-07 21:58
已编辑
广州市第二中学 Java
点赞 评论 收藏
分享
评论
3
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务