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

面试高频缓存算法:LRU 与 LFU 原理及 Java 实现

LRU 缓存算法通过哈希表与双向链表结合,实现 O(1) 时间复杂度的查找与更新。最近使用的节点置于链表头部,淘汰时移除尾部节点。LFU 算法则基于访问频率淘汰,常见实现包括哈希表加平衡二叉树(TreeSet)以及双哈希表结构。后者通过按频率分组的双向链表进一步优化至纯 O(1) 操作。两者均为面试常考数据结构应用,重点在于边界处理与哨兵节点的使用。

萤火微光发布于 2026/3/24更新于 2026/10/2889 浏览
面试高频缓存算法:LRU 与 LFU 原理及 Java 实现

一、LRU 缓存算法

1. 哈希表 + 双向链表

问题背景 设计一个满足 LRU(最近最少使用)原则的缓存结构。当缓存达到容量上限时,需要淘汰最久未使用的元素。

核心思路 单纯使用数组或链表无法同时满足 O(1) 的查找和删除效率。最佳方案是组合数据结构:

  • 哈希表(HashMap):存储 key 到节点的映射,实现 O(1) 快速查找。
  • 双向链表:维护节点的使用顺序。最近访问的节点移至头部,最久未使用的在尾部,淘汰时直接移除尾部。
  • 哑头/哑尾节点:作为哨兵,简化边界处理,避免判断空指针或首尾节点的特殊逻辑。

代码实现

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;
    private DLinkedNode tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        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 tailNode = removeTail();
                cache.remove(tailNode.key);
                size--;
            }
        } else {
            node.value = value;
            moveToHead(node);
        }
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    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;
    }
}

关键点解析

  • moveToHead 是 LRU 的核心,每次访问或更新都需将节点提到头部。
  • 淘汰逻辑发生在 put 且 size > capacity 时,直接移除 tail.prev。
  • 节点中必须保存 key,以便从哈希表中同步删除被替换的节点。

二、LFU 缓存算法

LFU(最不经常使用)算法优先淘汰访问频率最低的元素。若频率相同,则淘汰最早访问的。

1. 哈希表 + 平衡二叉树

利用 TreeSet 按频率和时间戳排序,可快速获取待淘汰节点,但增删操作为 O(log n)。

核心逻辑

  • 每个节点记录访问次数 cnt 和时间戳 time。
  • TreeSet 根据 cnt 升序、time 升序排列,队首即为淘汰目标。
  • 更新节点时需先从集合移除,修改属性后再加入,以维持有序性。

代码实现

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 o) {
            if (this == o) return true;
            if (!(o instanceof Node)) return false;
            Node rhs = (Node) o;
            return this.cnt == rhs.cnt && this.time == rhs.time;
        }

        @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> keyTable;
    private TreeSet<Node> S;

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

    public int get(int key) {
        if (capacity == 0 || !keyTable.containsKey(key)) return -1;
        Node cache = keyTable.get(key);
        S.remove(cache);
        cache.cnt++;
        cache.time = ++time;
        S.add(cache);
        keyTable.put(key, cache);
        return cache.value;
    }

    public void put(int key, int value) {
        if (capacity == 0) return;
        if (!keyTable.containsKey(key)) {
            if (keyTable.size() == capacity) {
                Node minNode = S.first();
                keyTable.remove(minNode.key);
                S.remove(minNode);
            }
            Node cache = new Node(1, time++, key, value);
            keyTable.put(key, cache);
            S.add(cache);
        } else {
            Node cache = keyTable.get(key);
            S.remove(cache);
            cache.cnt++;
            cache.time = ++time;
            cache.value = value;
            S.add(cache);
            keyTable.put(key, cache);
        }
    }
}

2. 双哈希表优化

为了达到纯 O(1) 复杂度,可使用双哈希表配合双向链表。

设计思路

  • keyTable:Key -> Node,用于快速定位。
  • freqTable:Frequency -> DoublyLinkedList,同一频率的节点挂在同一个链表中。
  • minfreq:记录当前最小频率,淘汰时直接定位该频率组。
  • 节点访问后,从原频率链表移除,加入新频率链表头部。

代码实现

class LFUCache {
    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.next = node.next;
            node.next.prev = node.prev;
            size--;
        }

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

    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++;
        }

        DoublyLinkedList list = freqTable.getOrDefault(freq + 1, new DoublyLinkedList());
        list.addFirst(new Node(key, val, freq + 1));
        freqTable.put(freq + 1, list);
        keyTable.put(key, list.getHead());
        return val;
    }

    public void put(int key, int value) {
        if (capacity == 0) return;
        if (!keyTable.containsKey(key)) {
            if (keyTable.size() == capacity) {
                DoublyLinkedList list = freqTable.get(minfreq);
                Node node = list.getTail();
                keyTable.remove(node.key);
                list.remove(node);
                if (list.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, list.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++;
            }
            DoublyLinkedList list = freqTable.getOrDefault(freq + 1, new DoublyLinkedList());
            list.addFirst(new Node(key, value, freq + 1));
            freqTable.put(freq + 1, list);
            keyTable.put(key, list.getHead());
        }
    }
}

总结

  • LRU 适合数据局部性明显、近期访问概率高的场景。
  • LFU 适合长期访问频率稳定的场景。
  • 双哈希表实现的 LFU 在时间复杂度上优于 TreeSet 方案,是更优的工程实践。

目录

  1. 一、LRU 缓存算法
  2. 1. 哈希表 + 双向链表
  3. 二、LFU 缓存算法
  4. 1. 哈希表 + 平衡二叉树
  5. 2. 双哈希表优化

更多推荐文章

查看全部
  • 内网穿透实战:让本地 OpenClaw 服务随时随地上线
  • Coze 抓取小红书爆款视频写入飞书多维表实战
  • 医疗 AI 败血症预测算法全流程实现与代码解析
  • 大模型技术从基础入门到实战应用指南
  • AIOps 实践:基于 Dify + LangBot 构建飞书智能体机器人
  • 2024 年中国人工智能系列白皮书:体育人工智能
  • Flutter eip55 库鸿蒙适配指南:以太坊地址校验与验证
  • 生物医学Go编程:高性能计算与精准医疗案例分析 (一)
  • OpenClaw v7.0.0 桌面版首发,支持本地 AI 自动化代理
  • 国产麒麟操作系统 V10 X86 架构离线安装 Nginx 及配置管理
  • 双指针算法专题:移动零与复写零
  • Git 本地核心操作:Commit 规范、Reset 回退与 Restore 撤销
  • AI 大模型如何重塑就业生态与产业机遇
  • 链表两两交换:Java 递归与迭代三种实现方案
  • 算法实战:寻找数组中心下标与除自身外数组乘积(前缀和技巧)
  • MM1 多模态大模型预训练方法、分析与见解
  • 基于 SpringBoot 的图书租借系统设计与实现
  • Dify 工作流发布为 MCP Server 实战指南
  • Docker Desktop + WSL2 安装配置与核心应用实战
  • 寻找数组中心下标与除自身以外数组的乘积 - 前缀和技巧

相关免费在线工具

  • 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