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

深入解析 LRU 与 LFU 缓存算法:原理、实现与面试实战

LRU 和 LFU 是面试中常见的缓存淘汰策略。LRU 基于最近最少使用原则,通过哈希表与双向链表结合实现 O(1) 复杂度。LFU 则关注访问频率,常用双哈希表优化至 O(1)。本文详解两种算法的核心逻辑、数据结构设计及 Java 代码实现,帮助理解底层机制并应对技术面试。

城市逃兵发布于 2026/3/21更新于 2026/7/2340 浏览
深入解析 LRU 与 LFU 缓存算法:原理、实现与面试实战

一、LRU 缓存算法

1. 哈希表 + 双向链表

在面试和实际系统设计中,LRU(Least Recently Used)是最经典的缓存淘汰策略。它的核心需求是:支持 O(1) 时间复杂度的查找、插入和删除操作。

单纯用数组或链表无法满足,单纯用哈希表又无法维护访问顺序。因此,最佳实践是将哈希表与双向链表结合使用。

核心设计思路
  • 双向链表:维护缓存节点的访问顺序。最近使用的节点放在头部,最少使用的节点放在尾部。淘汰时直接删尾部。
  • 哈希表:存储 key 到链表节点的映射,实现 O(1) 快速查找,避免遍历链表。
  • 哑头/哑尾节点:简化边界处理,无需判断'链表是否为空'或'节点是否为头/尾'。
关键规则
  1. 访问/更新节点:将节点移到链表头部(标记为'最近使用')。
  2. 新增节点:添加到链表头部,若容量超限,删除链表尾部节点(最少使用),并同步删除哈希表中的映射。
代码实现
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 int capacity;
    private DLinkedNode head;
    private DLinkedNode tail;

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

    // 4. get 操作:根据 key 获取缓存值
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        moveToHead(node);
        return node.value;
    }

    // 5. 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);
        }
    }

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

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

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

    // 9. 辅助方法:移除链表尾部的真实节点(最少使用的节点),并返回该节点
    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}
代码分析要点
  • DLinkedNode:内部类封装了键值对及指针。注意 key 必须保存,因为淘汰时需要通过它从哈希表中删除映射,仅存 value 无法反向查找。
  • 哑节点:head 和 tail 是哨兵,让增删逻辑统一,不用单独处理空链表或单节点情况。
  • moveToHead:这是 LRU 的核心体现。每次访问或更新,都要把节点提到最前面,保证尾部永远是'最久未使用'的。

二、LFU 缓存算法

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

1. 哈希表 + 平衡二叉树

这种方案利用 TreeSet 来维护有序性,按频率和时间戳排序。

核心逻辑
  • HashMap:实现 O(1) 查找节点。
  • TreeSet:按「频率 + 时间戳」排序,第一个元素即为待淘汰节点。
  • 排序规则:先按频率升序,频率相同则按时间戳升序。
代码实现
class LFUCache {
    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);
        }
    }
}

2. 双哈希表(优化方案)

上述 TreeSet 方案的时间复杂度是 O(logN)。为了达到纯 O(1),我们采用双哈希表 + 双向链表的方案。

算法设计
  • keyTable:键 → 节点,用于快速查找。
  • freqTable:频率 → 双向链表,同一频率的节点存在同一个链表中。
  • 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;
        int val;
        int freq;
        Node prev;
        Node 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;
        Node 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 排序。minfreq 快速定位淘汰目标。只有当原频率链表为空且等于 minfreq 时,才更新 minfreq。


三、总结

LRU 和 LFU 都是面试中高频考察的缓存淘汰算法。

  • LRU 侧重于'最近使用',适合数据访问具有局部性的场景,实现简单高效。
  • LFU 侧重于'历史频率',适合某些数据长期被频繁访问的场景。

理解这两种算法背后的数据结构组合(哈希表 + 链表 vs 哈希表 + 平衡树),对于掌握系统设计中的性能优化至关重要。

目录

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

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

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

更多推荐文章

查看全部
  • LangChain 快速入门:从环境搭建到链式调用实战
  • JDK 1.6 至 25 版本支持平台说明
  • 【异常】飞书OpenClaw机器人 HTTP 401: Invalid Authentication 报错排查与解决方案
  • 双指针经典算法题实战解析:从原理到代码
  • 网络通信与 TCP/IP 五层模型
  • FPGA 比特流深度解析
  • 爬虫与大模型结合实践:从基础提取到框架应用
  • 远程工具横评:UU 远程 2026 年 1 月升级体验与竞品对比
  • 基于 Java 和 Leaflet 的湖南省道路长度 WebGIS 构建
  • C++ STL 容器详解:map 与 set 核心用法与底层逻辑
  • Visual C++ MFC 基础图形绘制实战:点线面与投影
  • Java 反射与方法句柄:动态编程深度解析
  • Python 七大主流职业方向与技术栈详解
  • 点云预测作为 4D 占用预测代理(一)
  • 自然语言处理在法律领域的应用与实战
  • 人工智能 AI 产品经理与传统产品经理的差异分析
  • 计算机视觉基础、模型架构与实战应用
  • AI 绘画在商业设计中的应用与代码实践
  • MiniRAG:轻量级检索增强生成方法与异构图索引技术
  • 大模型训练技术架构、并行策略与优化方案详解

相关免费在线工具

  • 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