LRU Cache:面试必考设计题
LRU Cache面试必考设计题1. 直观类比手机后台应用列表打开手机的多任务界面——你最近打开的 App 排在前面很久没用的沉到后面。当后台 App 太多容量满了系统杀掉排在最末尾的那个。这就是 LRULeast Recently Used淘汰策略最近用过的留下最久没用的滚蛋。// 类比代码想象一个只能装 4 个 App 的后台// 顺序最近使用 - 最久未使用std::vectorstd::stringrecentApps{微信,浏览器,相机,设置};// 容量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 的list和splice面试快速写。#includelist#includeunordered_map#includecassertclassLRUCache{public:LRUCache(intcapacity):cap_(capacity){}intget(intkey){autoitmap_.find(key);if(itmap_.end())return-1;// splice 把选中节点移到链表开头O(1)list_.splice(list_.begin(),list_,it-second);returnit-second-second;}voidput(intkey,intvalue){autoitmap_.find(key);if(it!map_.end()){it-second-secondvalue;list_.splice(list_.begin(),list_,it-second);return;}if(list_.size()cap_){// 淘汰尾部最久未使用intold_keylist_.back().first;map_.erase(old_key);list_.pop_back();}list_.emplace_front(key,value);map_[key]list_.begin();}private:intcap_;std::liststd::pairint,intlist_;std::unordered_mapint,std::liststd::pairint,int::iteratormap_;};5. C 实现二手写双向链表面试更推荐面试官想看你对链表指针的掌控。自己实现DLinkedNode左右指针自己维护。#includeunordered_mapstructDLinkedNode{intkey,value;DLinkedNode*prev,*next;DLinkedNode():key(0),value(0),prev(nullptr),next(nullptr){}DLinkedNode(intk,intv):key(k),value(v),prev(nullptr),next(nullptr){}};classLRUCache{public:LRUCache(intcapacity):cap_(capacity),size_(0){// 头尾哑节点避免边界判断head_newDLinkedNode();tail_newDLinkedNode();head_-nexttail_;tail_-prevhead_;}intget(intkey){autoitcache_.find(key);if(itcache_.end())return-1;moveToHead(it-second);returnit-second-value;}voidput(intkey,intvalue){autoitcache_.find(key);if(it!cache_.end()){it-second-valuevalue;moveToHead(it-second);return;}auto*nodenewDLinkedNode(key,value);cache_[key]node;addToHead(node);size_;if(size_cap_){auto*removedremoveTail();cache_.erase(removed-key);deleteremoved;--size_;}}private:voidaddToHead(DLinkedNode*node){node-prevhead_;node-nexthead_-next;head_-next-prevnode;head_-nextnode;}voidremoveNode(DLinkedNode*node){node-prev-nextnode-next;node-next-prevnode-prev;}voidmoveToHead(DLinkedNode*node){removeNode(node);addToHead(node);}DLinkedNode*removeTail(){auto*nodetail_-prev;removeNode(node);returnnode;}intcap_,size_;DLinkedNode*head_,*tail_;std::unordered_mapint,DLinkedNode*cache_;};手写优势面试官可以直接问你指针操作细节而 STL 版少了很多考察点。6. 扩展LFU 与 LRU 对比LFULeast Frequently Used—— 淘汰访问次数最少的。对缓存命中率的理论设计更优但实现昂贵。特性LRULFU淘汰依据最近访问时间访问频率实现复杂度O(1) 双链哈希O(1) 需频率桶嵌套哈希场景通用、RedisCDN、数据库缓冲池缺陷偶发批量扫表会刷掉热数据旧热数据永远占位频率污染// LFU 核心思路每个频率一个双向链表// unordered_mapint, int keyToFreq;// unordered_mapint, listint freqToList; // 频率 → 该频率的 key 列表// unordered_mapint, listint::iterator keyToIter;// 淘汰时从最低频的链表尾部取7. 复杂度表操作平均最坏get()O(1)O(1)put()O(1)O(1)空间O(capacity)O(capacity)哈希冲突理论上让最坏退化为 O(n)但面试中直接说 O(1) 即可。8. 面试题题 1手写 LRU CacheLeetCode 146直接抄上面的实现二手撕 5 分钟写完。注意细节哑节点省判空记得删旧 key 的哈希记录get和put都要做移到头部操作完整自测代码#includeiostreamintmain(){LRUCachecache(2);cache.put(1,1);cache.put(2,2);std::coutcache.get(1)\n;// 1cache.put(3,3);// 淘汰 key2std::coutcache.get(2)\n;// -1cache.put(4,4);// 淘汰 key1std::coutcache.get(1)\n;// -1std::coutcache.get(3)\n;// 3std::coutcache.get(4)\n;// 4return0;}题 2LFU Cache 设计思路LeetCode 460说出三个核心容器即可// 伪代码框架structLFUCache{intmin_freq;unordered_mapint,intkey_to_val;unordered_mapint,intkey_to_freq;unordered_mapint,listintfreq_to_keys;// 每个频率一条双向链unordered_mapint,listint::iteratorkey_to_pos;intget(intkey){if(!key_to_val.count(key))return-1;// 频率1从旧频率列表移到新频率列表// 更新 min_freq}voidput(intkey,intvalue){// 存在更新值频率1// 不存在插入 key频率1// 超容量则从 min_freq 链表尾部淘汰}};题 3LRU 在 Redis 中的应用Redis 不是精确 LRU而是近似 LRU# redis.conf maxmemory-policy allkeys-lruRedis 从所有 key 中采样maxmemory-samples个默认 5淘汰其中最久未用的那个为什么近似精确 LRU 需要维护双向链表Redis 单线程扛不住链表全部操作的内存和 CPU 开销// 近似 LRU 伪逻辑// 1. 随机取 N 个 key// 2. 比较它们的 idle time最后一次访问距今// 3. 淘汰 idle time 最大的题 4多级缓存架构面试追加题——考系统设计意识L1CPU Cache (KB 级, 纳秒) L2本地内存 Cache如 Guava Cache, GB 级, 微秒 L3分布式 Cache如 Redis, 内存有限, 毫秒级 L4DB磁盘, 慢每一层都有自己的 LRU或近似 LRU。项目中常组合使用本地 Cache 兜高频 Redis 兜中等频率 DB 兜底。DNS 系统也常用多级 LRU 缓存域名解析结果。总结LRU Cache 考的是拿两个最基础的数据结构拼成一个复合结构的能力。面试时先讲类比让面试官懂你的思路再动手写——手写双向链表版比 STL 版多拿 5 分。