51mee - AI智能招聘平台Logo
模拟面试题目大全招聘中心会员专区

请设计一个高效的LRU(最近最少使用)缓存淘汰算法,并说明其时间复杂度。结合教育系统中课程推荐或习题的缓存场景,解释如何实现并发安全。

好未来C++难度:中等

答案

1) 【一句话结论】
LRU缓存通过哈希表+双向链表实现,支持O(1)时间访问、插入和淘汰,采用细粒度锁(哈希表查找不加锁,仅操作加锁)保证并发安全,适用于教育系统中课程/习题的快速推荐场景。

2) 【原理/概念讲解】
老师会解释核心数据结构的作用:

  • 哈希表(unordered_map):存储键值对(如课程ID→课程信息),提供O(1)时间查找,相当于“索引字典”,快速定位元素。
  • 双向链表:维护访问顺序,链表头是最近访问的元素,链表尾是最久未访问的元素。删除尾节点时,通过虚拟尾节点和指针直接操作(如tail->prev->next = tail->prev->prev; tail->prev = tail->prev->prev;),无需遍历,时间复杂度为O(1),类比“书架上的书籍,最久未读的从后端直接取下”。
  • 操作逻辑:访问元素时,将其从链表中移到头(更新位置);插入新元素时也放在头;淘汰时移除链表尾的元素(即最久未访问的)。

3) 【对比与适用场景】

策略定义特性使用场景注意点
LRU淘汰最久未访问的元素哈希表+双向链表,O(1)访问/插入/淘汰;维护访问顺序教育系统课程推荐(高频课程保留)、习题缓存(高频习题优先)需高效数据结构,并发时需加锁
FIFO先进先出哈希表+单链表,O(1)插入/淘汰;按时间顺序临时文件缓存(如系统临时文件)可能不符合实际访问模式(如刚访问的元素被淘汰)
LFU淘汰最少使用频率的元素哈希表+频率计数+双向链表,O(1)访问/插入/淘汰;维护频率频繁访问的元素(如高频习题)初始阶段计数不准确,需额外数据结构

4) 【示例】
用智能指针管理链表节点,避免内存泄漏。伪代码:

#include <unordered_map>
#include <memory>
#include <iostream>

struct Node {
    int key;
    int value;
    std::unique_ptr<Node> prev;
    Node* next; // 指向后继节点
};

class LRUCache {
private:
    std::unordered_map<int, std::unique_ptr<Node>> cache;
    Node* head = new Node(); // 虚拟头节点
    Node* tail = new Node(); // 虚拟尾节点
    int capacity;
    int size;

    void addNode(std::unique_ptr<Node> node) {
        node->next = head->next;
        head->next->prev = node.get();
        head->next = node.get();
    }

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

    void moveToHead(std::unique_ptr<Node> node) {
        removeNode(node.get());
        addNode(std::move(node));
    }

public:
    LRUCache(int capacity) : capacity(capacity), size(0) {
        head->next = tail;
        tail->prev = head;
    }

    int get(int key) {
        auto it = cache.find(key);
        if (it == cache.end()) return -1;
        auto node = std::move(it->second);
        moveToHead(std::move(node));
        return node->value;
    }

    void put(int key, int value) {
        auto it = cache.find(key);
        if (it != cache.end()) {
            auto node = std::move(it->second);
            node->value = value;
            moveToHead(std::move(node));
        } else {
            if (size == capacity) {
                auto tailNode = tail->prev;
                removeNode(tailNode);
                cache.erase(tailNode->key);
            }
            auto newNode = std::make_unique<Node>();
            newNode->key = key;
            newNode->value = value;
            moveToHead(std::make_unique<Node>(std::move(newNode)));
            cache[key] = std::move(newNode);
            size++;
        }
    }
};

5) 【面试口播版答案】
好的,面试官。我设计的LRU缓存算法核心是哈希表+双向链表结构,保证O(1)时间复杂度的访问、插入和淘汰操作。哈希表用于快速查找元素(相当于索引字典,快速定位课程信息),双向链表维护访问顺序(最近访问的元素放在链表头,最久未访问的放在尾)。当用户访问“数学基础”课程时,若该课程在缓存中,则将其移动到链表头;若未在缓存中,则插入到头并淘汰链表尾的元素(如“物理习题”)。在并发场景下,我会用细粒度锁:哈希表查找操作不加锁(因为只是读取,不修改数据),而插入、删除、移动节点时加锁,确保多线程安全。这样在教育系统中,课程推荐缓存能快速响应高频请求,提升用户体验。

6) 【追问清单】

  • 问:并发安全中为什么哈希表查找不加锁?
    答:查找只是读取数据,不修改哈希表或链表,不加锁可提升读性能。
  • 问:为什么用双向链表删除尾节点是O(1)?
    答:通过虚拟尾节点和指针直接操作,无需遍历链表,时间复杂度为O(1)。
  • 问:内存管理上如何避免泄漏?
    答:使用智能指针(如unique_ptr)管理链表节点,自动释放内存。
  • 问:如果多个线程同时访问同一个key,get操作是否安全?
    答:get操作仅查找哈希表,不加锁,因为数据不会改变,但返回后移动节点时加锁,保证原子性。
  • 问:LRU的淘汰策略是否正确处理了“刚访问后立即淘汰”的情况?
    答:访问后立即移动到头,所以刚访问的元素不会被淘汰,淘汰的是最久未访问的。

7) 【常见坑/雷区】

  • 坑1:时间复杂度证明不足,只说O(1)但没解释哈希表和链表的操作,面试官会追问“为什么哈希表查找是O(1)”或“链表操作为什么是O(1)”。
  • 坑2:并发锁粒度错误,比如对整个LRU加锁,导致读操作也阻塞,降低性能。
  • 坑3:链表操作错误,比如插入头时没更新哈希表,导致后续get失败(如put后get找不到)。
  • 坑4:适用场景描述不准确,比如说LRU适合所有缓存,而实际上FIFO更适合临时文件缓存,需区分场景。
  • 坑5:内存泄漏,未用智能指针管理链表节点,导致程序崩溃。
51mee.com致力于为招聘者提供最新、最全的招聘信息。AI智能解析岗位要求,聚合全网优质机会。
产品招聘中心面经会员专区简历解析Resume API
联系我们南京浅度求索科技有限公司admin@51mee.com
联系客服
51mee客服微信二维码 - 扫码添加客服获取帮助
© 2025 南京浅度求索科技有限公司. All rights reserved.
公安备案图标苏公网安备32010602012192号苏ICP备2025178433号-1