LRU Cache:面试必考设计题

LRU Cache:面试必考设计题

1. 直观类比:手机后台应用列表

打开手机的多任务界面——你最近打开的 App 排在前面,很久没用的沉到后面。当后台 App 太多(容量满了),系统杀掉排在最末尾的那个。

这就是 LRU(Least Recently Used)淘汰策略:最近用过的留下,最久没用的滚蛋

// 类比代码:想象一个只能装 4 个 App 的后台
// 顺序:最近使用 -> 最久未使用
std::vector<std::string> recentApps = {"微信", "浏览器", "相机", "设置"};
// 容量=4,新开"小红书" → 淘汰"设置"

2. 数据结构选择:为什么两个一起上?

只用一个数据结构搞不定,逐个分析:

数据结构能 O(1) 查找?能维护顺序?
哈希表❌ 无顺序概念
单向链表❌ O(n) 遍历✅ 有先后
双向链表❌ 同上✅ 可 O(1) 删节点
哈希表 + 双向链表

哈希表给 O(1) 查找,双向链表给 O(1) 插入/删除。组合起来就是 LRU Cache 的标准答案。

哈希表 key→节点指针

双向链表 头 ←→ 尾

头: 最近使用

尾: 最久未使用

3. 核心操作

get(key):
  不存在 → return -1
  存在   → 把节点移到链表头部,返回 value

put(key, value):
  已存在 → 更新 value,移到头部
  不存在 → 插入头部
           检查容量,超了则删尾部

put(key,val) 开始

key 是否已存在?

存在:更新 value
移到链表头部

不存在:插入头部

容量是否已满?

删除尾部节点
同时删哈希表记录

完成

4. C++ 实现一:std::list + unordered_map

利用 STL 的 listsplice,面试快速写。

#include <list>
#include <unordered_map>
#include <cassert>

class LRUCache {
public:
    LRUCache(int capacity) : cap_(capacity) {}

    int get(int key) {
        auto it = map_.find(key);
        if (it == map_.end()) return -1;
        // splice 把选中节点移到链表开头,O(1)
        list_.splice(list_.begin(), list_, it->second);
        return it->second->second;
    }

    void put(int key, int value) {
        auto it = map_.find(key);
        if (it != map_.end()) {
            it->second->second = value;
            list_.splice(list_.begin(), list_, it->second);
            return;
        }
        if (list_.size() == cap_) {
            // 淘汰尾部(最久未使用)
            int old_key = list_.back().first;
            map_.erase(old_key);
            list_.pop_back();
        }
        list_.emplace_front(key, value);
        map_[key] = list_.begin();
    }

private:
    int cap_;
    std::list<std::pair<int, int>> list_;
    std::unordered_map<int, std::list<std::pair<int, int>>::iterator> map_;
};

5. C++ 实现二:手写双向链表(面试更推荐)

面试官想看你对链表指针的掌控。自己实现 DLinkedNode,左右指针自己维护。

#include <unordered_map>

struct DLinkedNode {
    int key, value;
    DLinkedNode *prev, *next;
    DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};

class LRUCache {
public:
    LRUCache(int capacity) : cap_(capacity), size_(0) {
        // 头尾哑节点,避免边界判断
        head_ = new DLinkedNode();
        tail_ = new DLinkedNode();
        head_->next = tail_;
        tail_->prev = head_;
    }

    int get(int key) {
        auto it = cache_.find(key);
        if (it == cache_.end()) return -1;
        moveToHead(it->second);
        return it->second->value;
    }

    void put(int key, int value) {
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            it->second->value = value;
            moveToHead(it->second);
            return;
        }
        auto* node = new DLinkedNode(key, value);
        cache_[key] = node;
        addToHead(node);
        ++size_;
        if (size_ > cap_) {
            auto* removed = removeTail();
            cache_.erase(removed->key);
            delete removed;
            --size_;
        }
    }

private:
    void addToHead(DLinkedNode* node) {
        node->prev = head_;
        node->next = head_->next;
        head_->next->prev = node;
        head_->next = node;
    }

    void removeNode(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(DLinkedNode* node) {
        removeNode(node);
        addToHead(node);
    }

    DLinkedNode* removeTail() {
        auto* node = tail_->prev;
        removeNode(node);
        return node;
    }

    int cap_, size_;
    DLinkedNode *head_, *tail_;
    std::unordered_map<int, DLinkedNode*> cache_;
};

手写优势:面试官可以直接问你指针操作细节,而 STL 版少了很多考察点。

6. 扩展:LFU 与 LRU 对比

LFU(Least Frequently Used)—— 淘汰访问次数最少的。对缓存命中率的理论设计更优,但实现昂贵。

特性LRULFU
淘汰依据最近访问时间访问频率
实现复杂度O(1) 双链+哈希O(1) 需频率桶+嵌套哈希
场景通用、RedisCDN、数据库缓冲池
缺陷偶发批量扫表会刷掉热数据旧热数据永远占位(频率污染)
// LFU 核心思路:每个频率一个双向链表
// unordered_map<int, int> keyToFreq;
// unordered_map<int, list<int>> freqToList;  // 频率 → 该频率的 key 列表
// unordered_map<int, list<int>::iterator> keyToIter;
// 淘汰时从最低频的链表尾部取

7. 复杂度表

操作平均最坏
get()O(1)O(1)
put()O(1)O(1)
空间O(capacity)O(capacity)

哈希冲突理论上让最坏退化为 O(n),但面试中直接说 O(1) 即可。

8. 面试题

题 1:手写 LRU Cache(LeetCode 146)

直接抄上面的实现二,手撕 5 分钟写完。注意细节:

  • 哑节点省判空
  • 记得删旧 key 的哈希记录
  • getput 都要做"移到头部"操作

完整自测代码:

#include <iostream>

int main() {
    LRUCache cache(2);
    cache.put(1, 1);
    cache.put(2, 2);
    std::cout << cache.get(1) << "\n";  // 1
    cache.put(3, 3);                     // 淘汰 key=2
    std::cout << cache.get(2) << "\n";  // -1
    cache.put(4, 4);                     // 淘汰 key=1
    std::cout << cache.get(1) << "\n";  // -1
    std::cout << cache.get(3) << "\n";  // 3
    std::cout << cache.get(4) << "\n";  // 4
    return 0;
}

题 2:LFU Cache 设计思路(LeetCode 460)

说出三个核心容器即可:

// 伪代码框架
struct LFUCache {
    int min_freq;
    unordered_map<int, int> key_to_val;
    unordered_map<int, int> key_to_freq;
    unordered_map<int, list<int>> freq_to_keys;  // 每个频率一条双向链
    unordered_map<int, list<int>::iterator> key_to_pos;

    int get(int key) {
        if (!key_to_val.count(key)) return -1;
        // 频率+1,从旧频率列表移到新频率列表
        // 更新 min_freq
    }
    void put(int key, int value) {
        // 存在:更新值,频率+1
        // 不存在:插入 key,频率=1
        // 超容量则从 min_freq 链表尾部淘汰
    }
};

题 3:LRU 在 Redis 中的应用

Redis 不是精确 LRU,而是近似 LRU

# redis.conf
maxmemory-policy allkeys-lru
  • Redis 从所有 key 中采样 maxmemory-samples 个(默认 5)
  • 淘汰其中最久未用的那个
  • 为什么近似:精确 LRU 需要维护双向链表,Redis 单线程扛不住链表全部操作的内存和 CPU 开销
// 近似 LRU 伪逻辑
// 1. 随机取 N 个 key
// 2. 比较它们的 idle time(最后一次访问距今)
// 3. 淘汰 idle time 最大的

题 4:多级缓存架构

面试追加题——考系统设计意识:

L1:CPU Cache (KB 级, 纳秒)
L2:本地内存 Cache(如 Guava Cache, GB 级, 微秒)
L3:分布式 Cache(如 Redis, 内存有限, 毫秒级)
L4:DB(磁盘, 慢)

每一层都有自己的 LRU(或近似 LRU)。项目中常组合使用:本地 Cache 兜高频 + Redis 兜中等频率 + DB 兜底。DNS 系统也常用多级 LRU 缓存域名解析结果。


总结:LRU Cache 考的是"拿两个最基础的数据结构拼成一个复合结构"的能力。面试时先讲类比让面试官懂你的思路,再动手写——手写双向链表版比 STL 版多拿 5 分。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ricky_Theseus

感谢大家,祝您生活愉快

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值