跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

深入理解 LRU 与 LFU 缓存算法实现

LRU 和 LFU 是面试中常见的缓存淘汰策略。LRU 基于最近访问时间,使用哈希表加双向链表实现 O(1) 操作。LFU 基于访问频率,可通过平衡树或双哈希表优化。两者在系统设计中各有适用场景,掌握其底层原理对应对高并发架构挑战至关重要。

KernelLab发布于 2026/3/30更新于 2026/7/2135 浏览
深入理解 LRU 与 LFU 缓存算法实现

深入理解 LRU 与 LFU 缓存算法实现

在系统设计和面试中,缓存淘汰策略是高频考点。LRU(最近最少使用)和 LFU(最不经常使用)代表了两种不同的淘汰逻辑,掌握它们的底层实现原理,对于应对高并发架构挑战至关重要。

一、LRU 缓存算法

1. 哈希表 + 双向链表

以 LeetCode 的 LRU Cache 题目为例,核心需求是在 O(1) 时间复杂度内完成 get 和 put 操作。

LRU 题目描述

设计思路

单纯用哈希表无法维护访问顺序,单纯用链表查找效率太低。因此我们采用组合方案:

  • 双向链表:维护节点的访问顺序。最近使用的节点放在头部,最少使用的节点放在尾部。淘汰时直接删除尾部节点。
  • 哈希表:存储 key 到链表节点的映射,实现 O(1) 快速查找。
  • 哑头/哑尾节点:简化边界处理,无需判断链表是否为空或节点是否为头尾。
代码实现
class LRUCache {
    // 1. 双向链表节点类:封装缓存的 key、value,以及前后指针
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;

        // 无参构造:用于创建哑头/哑尾节点
        public DLinkedNode() {}

        // 有参构造:用于创建真实的缓存节点
        public DLinkedNode(int _key, int _value) {
            this.key = _key;
            this.value = _value;
        }
    }

    // 2. LRUCache 核心成员变量
    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private int size;       // 当前缓存中的节点数量
    private  capacity;   
     DLinkedNode head; 
     DLinkedNode tail; 

    
      {
        .size = ;
        .capacity = capacity;
        head =  ();
        tail =  ();
        head.next = tail;
        tail.prev = head;
    }

    
       {
           cache.get(key);
         (node == ) {
             -;
        }
        
        moveToHead(node);
         node.value;
    }

    
       {
           cache.get(key);

         (node == ) {
            
                (key, value);
            cache.put(key, newNode);
            addToHead(newNode);
            size++;

            
             (size > capacity) {
                   removeTail();
                cache.remove(tailNode.key);
                size--;
            }
        }  {
            
            node.value = value;
            moveToHead(node);
        }
    }

    
       {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    
       {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    
       {
        removeNode(node);
        addToHead(node);
    }

    
     DLinkedNode  {
           tail.prev;
        removeNode(res);
         res;
    }
}
int
// 缓存的最大容量
private
// 链表哑头节点
private
// 链表哑尾节点
// 3. 构造方法:初始化 LRU 缓存
public
LRUCache
(int capacity)
this
0
this
new
DLinkedNode
new
DLinkedNode
// 4. get 操作:根据 key 获取缓存值
public
int
get
(int key)
DLinkedNode
node
=
if
null
return
1
// 存在则移到链表头部,标记为'最近使用'
return
// 5. put 操作:添加/更新缓存
public
void
put
(int key, int value)
DLinkedNode
node
=
if
null
// 情况 1:key 不存在,新增节点
DLinkedNode
newNode
=
new
DLinkedNode
// 若超过容量,淘汰尾部节点
if
DLinkedNode
tailNode
=
else
// 情况 2:key 已存在,更新缓存值
// 6. 辅助方法:删除链表中指定的节点(O(1) 时间)
private
void
removeNode
(DLinkedNode node)
// 7. 辅助方法:将节点添加到链表头部(O(1) 时间)
private
void
addToHead
(DLinkedNode node)
// 8. 辅助方法:将节点移到链表头部(先删除,再头插)
private
void
moveToHead
(DLinkedNode node)
// 9. 辅助方法:移除链表尾部的真实节点,并返回该节点
private
removeTail
()
DLinkedNode
res
=
return
关键点解析
  • 哑节点的作用:有了哑头哑尾,插入和删除逻辑就不需要单独处理'链表为空'或'操作头尾节点'的特殊情况,代码更简洁。
  • Key 的必要性:节点中必须存储 key。因为当需要从哈希表中删除映射时,我们只有链表节点,如果没有 key,就无法反向定位哈希表中的 entry。
  • 复杂度:所有操作均为 O(1),这是 LRU 缓存的最优实现。

二、LFU 缓存算法

LFU(Least Frequently Used)优先淘汰访问频率最低的元素。如果频率相同,则淘汰最早访问的元素。

1. 哈希表 + 平衡二叉树

这种方案借助 TreeSet 来维护节点的有序性。

LFU 题目描述

核心逻辑
  • HashMap:实现 O(1) 查找节点。
  • TreeSet:按「频率 + 时间戳」排序。第一个元素即为待淘汰节点。
  • 排序规则:先按频率升序,频率相同则按时间戳升序。
代码实现
class LFUCache {
    // 缓存节点类:实现 Comparable 用于 TreeSet 排序
    class Node implements Comparable<Node> {
        int cnt;      // 访问频率
        int time;     // 时间戳
        int key;
        int value;

        public Node(int cnt, int time, int key, int value) {
            this.cnt = cnt;
            this.time = time;
            this.key = key;
            this.value = value;
        }

        @Override
        public boolean equals(Object anObject) {
            if (this == anObject) return true;
            if (anObject instanceof Node) {
                Node rhs = (Node) anObject;
                return this.cnt == rhs.cnt && this.time == rhs.time;
            }
            return false;
        }

        @Override
        public int compareTo(Node rhs) {
            return cnt == rhs.cnt ? time - rhs.time : cnt - rhs.cnt;
        }

        @Override
        public int hashCode() {
            return cnt * 1000000007 + time;
        }
    }

    private int capacity;
    private int time;
    private Map<Integer, Node> key_table;
    private TreeSet<Node> S;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.time = 0;
        key_table = new HashMap<>();
        S = new TreeSet<>();
    }

    public int get(int key) {
        if (capacity == 0 || !key_table.containsKey(key)) {
            return -1;
        }
        Node cache = key_table.get(key);
        S.remove(cache); // 移除旧节点,因为属性要更新
        cache.cnt++;
        cache.time = ++time;
        S.add(cache);    // 重新加入
        return cache.value;
    }

    public void put(int key, int value) {
        if (capacity == 0) return;

        if (!key_table.containsKey(key)) {
            if (key_table.size() == capacity) {
                // 淘汰频率最低、最早访问的节点
                Node toRemove = S.first();
                key_table.remove(toRemove.key);
                S.remove(toRemove);
            }
            Node cache = new Node(1, time++, key, value);
            key_table.put(key, cache);
            S.add(cache);
        } else {
            Node cache = key_table.get(key);
            S.remove(cache);
            cache.cnt++;
            cache.time = ++time;
            cache.value = value;
            S.add(cache);
        }
    }
}
分析

此方案实现简单,但 TreeSet 的增删操作是 O(log N)。每次更新节点都需要先从集合中移除,修改属性后再插入,以保证排序正确。

2. 双哈希表优化

为了达到纯 O(1) 的时间复杂度,我们可以引入「频率桶」的概念。

设计思路
  • keyTable:键 → 节点,用于快速查找。
  • freqTable:频率 → 双向链表,同一频率的节点挂在同一个链表中。
  • minfreq:记录当前最小访问频率,直接定位待淘汰的频率组。

淘汰时,只需找到 minfreq 对应链表的尾节点即可。频率更新时,节点从原频率链表移除,加入新频率链表。

代码实现
class LFUCache {
    private int minfreq;
    private int capacity;
    private Map<Integer, Node> keyTable;
    private Map<Integer, DoublyLinkedList> freqTable;

    public LFUCache(int capacity) {
        this.minfreq = 0;
        this.capacity = capacity;
        keyTable = new HashMap<>();
        freqTable = new HashMap<>();
    }

    public int get(int key) {
        if (capacity == 0 || !keyTable.containsKey(key)) {
            return -1;
        }
        Node node = keyTable.get(key);
        int val = node.val;
        int freq = node.freq;

        // 从原频率链表移除
        freqTable.get(freq).remove(node);

        // 如果原频率链表为空,删除该频率映射,并更新 minfreq
        if (freqTable.get(freq).size == 0) {
            freqTable.remove(freq);
            if (minfreq == freq) {
                minfreq += 1;
            }
        }

        // 加入新频率链表
        DoublyLinkedList list = freqTable.getOrDefault(freq + 1, new DoublyLinkedList());
        list.addFirst(new Node(key, val, freq + 1));
        freqTable.put(freq + 1, list);
        keyTable.put(key, freqTable.get(freq + 1).getHead());

        return val;
    }

    public void put(int key, int value) {
        if (capacity == 0) return;

        if (!keyTable.containsKey(key)) {
            if (keyTable.size() == capacity) {
                // 淘汰 minfreq 对应链表的尾节点
                Node node = freqTable.get(minfreq).getTail();
                keyTable.remove(node.key);
                freqTable.get(minfreq).remove(node);
                if (freqTable.get(minfreq).size == 0) {
                    freqTable.remove(minfreq);
                }
            }
            DoublyLinkedList list = freqTable.getOrDefault(1, new DoublyLinkedList());
            list.addFirst(new Node(key, value, 1));
            freqTable.put(1, list);
            keyTable.put(key, freqTable.get(1).getHead());
            minfreq = 1;
        } else {
            // 逻辑同 get,只是更新 value
            Node node = keyTable.get(key);
            int freq = node.freq;
            freqTable.get(freq).remove(node);
            if (freqTable.get(freq).size == 0) {
                freqTable.remove(freq);
                if (minfreq == freq) {
                    minfreq += 1;
                }
            }
            DoublyLinkedList list = freqTable.getOrDefault(freq + 1, new DoublyLinkedList());
            list.addFirst(new Node(key, value, freq + 1));
            freqTable.put(freq + 1, list);
            keyTable.put(key, freqTable.get(freq + 1).getHead());
        }
    }

    class Node {
        int key, val, freq;
        Node prev, next;
        public Node() { this(-1, -1, 0); }
        public Node(int key, int val, int freq) {
            this.key = key; this.val = val; this.freq = freq;
        }
    }

    class DoublyLinkedList {
        Node dummyHead, dummyTail;
        int size;
        public DoublyLinkedList() {
            dummyHead = new Node();
            dummyTail = new Node();
            dummyHead.next = dummyTail;
            dummyTail.prev = dummyHead;
            size = 0;
        }
        public void addFirst(Node node) {
            Node prevHead = dummyHead.next;
            node.prev = dummyHead;
            dummyHead.next = node;
            node.next = prevHead;
            prevHead.prev = node;
            size++;
        }
        public void remove(Node node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
            size--;
        }
        public Node getHead() { return dummyHead.next; }
        public Node getTail() { return dummyTail.prev; }
    }
}
对比总结

相比 TreeSet 方案,双哈希表 + 双向链表实现了真正的 O(1) 时间复杂度。虽然代码量稍多,但在对性能要求极高的场景下,这是 LFU 缓存的最优解。

核心在于利用 minfreq 快速定位淘汰目标,并通过分组链表避免全局遍历。实际开发中,可根据具体场景权衡实现复杂度与性能需求。

目录

  1. 深入理解 LRU 与 LFU 缓存算法实现
  2. 一、LRU 缓存算法
  3. 1. 哈希表 + 双向链表
  4. 设计思路
  5. 代码实现
  6. 关键点解析
  7. 二、LFU 缓存算法
  8. 1. 哈希表 + 平衡二叉树
  9. 核心逻辑
  10. 代码实现
  11. 分析
  12. 2. 双哈希表优化
  13. 设计思路
  14. 代码实现
  15. 对比总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 双指针专题:快乐数与盛水最多的容器
  • 六款大模型应用开发常用工具库
  • 程序员提升工作效率的 10 个核心建议
  • Python 核心语法与实战入门
  • Python 环境安装与基础入门指南
  • GitHub Copilot SDK 构建云原生多智能体系统
  • 第五届长城杯 2025 Web 初赛 Writeup
  • 基于 Web 的高等学校科技成果管理系统开题报告
  • SpringBoot 配置文件详解:Properties 与 YAML 实战
  • 大模型训练与推理中的云存储技术实践
  • 大模型为何被视为深度学习的未来
  • 大模型相关技术综述
  • 大模型微调技术方法综述
  • Android 中高级面试核心考点汇总与解析
  • Python 文档处理实战:python-docx 库详解与应用
  • 网络安全渗透测试全流程与核心工具指南
  • 主流大模型架构全景:GPT/LLaMA/DeepSeek/Qwen 深度对比
  • AI 陪练式前端入门:从零构建第一个网页
  • 基于 Termux 与 AstrBot 在安卓手机部署 QQ 机器人方案
  • Arduino BLDC 自主巡逻机器人设计与实现(避障 + 路径规划)

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online