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

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

LRU 缓存淘汰最近最少使用数据,通过哈希表加双向链表实现 O(1) 操作。LFU 缓存淘汰频率最低的数据,可用哈希表加平衡树或双哈希表优化。详细对比两种算法的设计思路、核心代码逻辑及性能差异,适合面试准备与工程实践参考。

灰度发布发布于 2026/3/27更新于 2026/10/781 浏览
深入解析 LRU 与 LFU 缓存算法原理及实现

LRU 缓存算法

哈希表 + 双向链表

以 LeetCode 上的 LRU 缓存问题为例,核心需求是支持 get 和 put 操作,且时间复杂度需达到 O(1)。这通常通过组合哈希表和双向链表来实现。

设计思路

双向链表负责维护节点的访问顺序:最近使用的节点放在头部,最少使用的在尾部。当容量满时,直接删除尾部节点即可。哈希表则用于存储 key 到节点的映射,解决链表遍历查找慢的问题,确保 O(1) 的查找速度。

为了简化边界处理,通常会引入哑头(head)和哑尾(tail)节点。这样在插入或删除首尾节点时,无需判断链表是否为空或是否只有一个节点,逻辑更加统一。

核心规则很简单:

  • 访问/更新:将节点移到链表头部,标记为'最近使用'。
  • 新增:添加到头部,若超过容量,删除尾部节点并同步清理哈希表。
代码实现
class LRUCache {
    // 双向链表节点类:封装缓存的 key、value,以及前后指针
    class DLinkedNode {
        int key; // 缓存键(淘汰节点时需要通过 key 删除哈希表中的映射)
        int value; // 缓存值
        DLinkedNode prev; // 前驱节点(双向链表)
        DLinkedNode next; // 后继节点(双向链表)

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

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

    // 哈希表:key -> 节点(O(1) 查找)
    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private int size; // 当前缓存中的节点数量
    private int capacity; // 缓存的最大容量
    private DLinkedNode head; // 链表哑头节点(哨兵)
    private DLinkedNode tail; // 链表哑尾节点(哨兵)

    // 构造方法:初始化 LRU 缓存
    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    // get 操作:根据 key 获取缓存值
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        moveToHead(node); // 将节点移到链表头部
        return node.value;
    }

    // put 操作:添加/更新缓存
    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 tailNode = removeTail();
                cache.remove(tailNode.key);
                size--;
            }
        } else {
            node.value = value;
            moveToHead(node);
        }
    }

    // 辅助方法:删除链表中指定的节点(O(1) 时间)
    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    // 辅助方法:将节点添加到链表头部(O(1) 时间)
    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    // 辅助方法:将节点移到链表头部(先删除,再头插)
    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    // 辅助方法:移除链表尾部的真实节点(最少使用的节点),并返回该节点
    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}
关键点分析
  • DLinkedNode 内部类:除了存储 key 和 value,prev/next 指针支持 O(1) 时间的节点增删。特别注意,节点中必须存储 key,因为淘汰时我们需要通过 key 从哈希表中删除映射,仅存 value 无法反向查找。
  • 哑头/哑尾节点:这是工程实践中常用的技巧,避免了大量 if (head == null) 或 if (tail == null) 的判断,让增删逻辑统一。
  • 核心方法:moveToHead 保证了'最近使用的节点在头部',这是 LRU 规则的核心体现。removeTail 配合哈希表删除,确保了空间限制下的淘汰机制。

LFU 缓存算法

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

方案一:哈希表 + 平衡二叉树

借助 HashMap 实现 O(1) 查找,借助 TreeSet 实现节点的有序存储(按频率 + 时间戳排序)。TreeSet 的第一个元素即为待淘汰节点。

核心逻辑

需要自定义 Node 类实现 Comparable 接口,定义排序规则:先按频率升序,频率相同则按时间戳升序。每次访问或更新节点时,需先从 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);
        key_table.put(key, 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) {
                key_table.remove(S.first().key);
                S.remove(S.first());
            }
            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);
            key_table.put(key, cache);
        }
    }
}

此方案中,TreeSet 的增删操作是 O(log n),虽然不如纯哈希表快,但实现起来相对直观。

方案二:双哈希表优化

为了达到所有操作 O(1) 的时间复杂度,可以使用双层哈希表 + 双向链表。

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

淘汰时,删除 minfreq 对应链表的尾节点(该频率下最久未使用的节点)。频率更新时,节点从原频率链表移除,加入新频率链表;若原频率链表为空且等于 minfreq,则 minfreq 加 1。

代码实现
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);
        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) {
                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 {
            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 = node.prev;
            Node next = node.next;
            prev.next = next;
            next.prev = prev;
            size--;
        }

        public Node getHead() { return dummyHead.next; }
        public Node getTail() { return dummyTail.prev; }
    }
}

相比 TreeSet 方案,双哈希表方案将所有操作优化到了 O(1),是 LFU 缓存的最优实现。

总结

LRU 和 LFU 都是经典的缓存淘汰策略。LRU 基于访问时间,适合数据访问具有局部性的场景;LFU 基于访问频率,适合某些热点数据长期被频繁访问的场景。

在实际工程中,选择哪种方案取决于具体业务对时间复杂度的要求以及对内存开销的容忍度。理解其背后的数据结构设计,对于应对面试和优化系统性能都至关重要。

目录

  1. LRU 缓存算法
  2. 哈希表 + 双向链表
  3. 设计思路
  4. 代码实现
  5. 关键点分析
  6. LFU 缓存算法
  7. 方案一:哈希表 + 平衡二叉树
  8. 核心逻辑
  9. 代码实现
  10. 方案二:双哈希表优化
  11. 设计思路
  12. 代码实现
  13. 总结

更多推荐文章

查看全部
  • Windows 环境下 Git Bash 安装 tmux 与 Fish Shell 配置教程
  • 基于 Java 和 Leaflet 的湖南省道路长度 WebGIS 系统构建
  • 《Agent Runtime 工程化》第十二章 二开毕业项目:12.6 学习闭环复盘
  • Qwen2.5-VL 视觉理解案例:Ollama 部署解析设计稿生成前端代码
  • 基于 AI Studio 构建自定义爬虫方案
  • 单链表综合练习:删除指定值、反转链表与查找中间节点
  • Manacher 算法详解:线性时间求解最长回文子串
  • Microi 吾码与 JavaScript 协同开发实战
  • Linux 进程详解:从基础概念到实战操作
  • OpenRouter 实战指南:单 API 接入 500+ 模型
  • FPGA 时序逻辑电路优化技巧实战
  • Windows 平台 JDK 版本管理工具 JVMS 使用指南
  • Python GUI 开发:Kivy 库详解与实战入门
  • 用老 Mac 跑本地 AI:OpenClaw 环境一键搭建
  • Higress 将现有 REST API 转换为 MCP Server 工具
  • 常见排序算法详解:冒泡、选择与插入
  • JDK 主流版本现状与选型建议
  • 双指针算法:快乐数判断
  • 自然语言处理在法律领域的应用与实战
  • Apache IoTDB 时序数据库核心特性与选型指南

相关免费在线工具

  • 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