【JavaScript面试题-算法与数据结构】手写一个 LRU(最近最少使用)缓存类,支持 `get` 和 `put` 操作,要求时间复杂度 O(1)

一、数据结构设计

为了实现 getput 操作的时间复杂度 O(1) ,我们组合了两种数据结构:

  1. 哈希表Map 或普通对象)

    • 存储键到双向链表节点的映射。
    • 作用:通过 key 直接定位到节点,实现 O(1) 的查找。
  2. 双向链表Node 类实现)

    • 维护所有节点的 使用顺序:链表头部(head 之后)是最近使用的节点,链表尾部(tail 之前)是最久未使用的节点。
    • 作用:在 O(1) 时间内完成节点的 移动到头部删除尾部 等操作。

此外,使用两个 哨兵节点 (伪头 head 和伪尾 tail),避免处理链表为空或只有一个节点时的边界条件,使插入和删除操作更简洁。

以下是 JavaScript 手写的 LRU 缓存类,使用哈希表 + 双向链表实现,确保 getput 操作时间复杂度为 O(1):

javascript

kotlin 复制代码
class LRUCache {
    /**
     * @param {number} capacity 缓存容量
     */
    constructor(capacity) {
        this.capacity = capacity;
        this.cache = new Map(); // 键 → 节点
        // 创建哨兵头尾节点,简化边界处理
        this.head = new Node(0, 0);
        this.tail = new Node(0, 0);
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

    /**
     * 获取键对应的值,并将该节点移动到链表头部(最近使用)
     * @param {number} key
     * @return {number}
     */
    get(key) {
        if (!this.cache.has(key)) {
            return -1;
        }
        const node = this.cache.get(key);
        this._moveToHead(node);
        return node.value;
    }

    /**
     * 插入或更新键值对,并将节点置于头部(最近使用)
     * 若容量超限,删除尾部节点(最久未使用)
     * @param {number} key
     * @param {number} value
     * @return {void}
     */
    put(key, value) {
        if (this.cache.has(key)) {
            // 已存在:更新值并移到头部
            const node = this.cache.get(key);
            node.value = value;
            this._moveToHead(node);
        } else {
            // 不存在:新建节点
            if (this.cache.size === this.capacity) {
                // 容量已满,删除尾部节点(最久未使用)
                const tailNode = this.tail.prev;
                this._removeNode(tailNode);
                this.cache.delete(tailNode.key);
            }
            const newNode = new Node(key, value);
            this.cache.set(key, newNode);
            this._addToHead(newNode);
        }
    }

    /**
     * 将节点从原位置移除,并添加到头部
     * @param {Node} node
     */
    _moveToHead(node) {
        this._removeNode(node);
        this._addToHead(node);
    }

    /**
     * 从链表中移除节点
     * @param {Node} node
     */
    _removeNode(node) {
        const prev = node.prev;
        const next = node.next;
        prev.next = next;
        next.prev = prev;
    }

    /**
     * 将节点插入到哨兵头节点之后(头部)
     * @param {Node} node
     */
    _addToHead(node) {
        node.prev = this.head;
        node.next = this.head.next;
        this.head.next.prev = node;
        this.head.next = node;
    }
}

/**
 * 双向链表节点
 */
class Node {
    constructor(key, value) {
        this.key = key;
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}

使用示例

javascript

scss 复制代码
const lru = new LRUCache(2);
lru.put(1, 1);      // 缓存: {1=1}
lru.put(2, 2);      // 缓存: {1=1, 2=2}
console.log(lru.get(1)); // 返回 1,并移动 1 到头部 → 缓存顺序: 2,1
lru.put(3, 3);      // 容量已满,删除尾部 2 → 缓存: {1=1, 3=3}
console.log(lru.get(2)); // 返回 -1 (未找到)
lru.put(4, 4);      // 容量已满,删除尾部 1 → 缓存: {3=3, 4=4}
console.log(lru.get(1)); // 返回 -1
console.log(lru.get(3)); // 返回 3
console.log(lru.get(4)); // 返回 4

复杂度说明

  • get: 哈希表查找 O(1) + 链表移动 O(1) → 总体 O(1)
  • put: 哈希表插入/更新 O(1) + 可能删除尾部 O(1) + 链表操作 O(1) → 总体 O(1)
相关推荐
im_AMBER1 小时前
AJAX vs Fetch API:Promise 与异步 JavaScript 怎么用?
前端·javascript·面试
用户9714171814271 小时前
JavaScript 作用域与作用域链详解
javascript
用户9751470751361 小时前
关于通过react使用hooks进行数据状态处理
前端
GISer_Jing1 小时前
React:从SPA到全场景渲染的进化之路
前端·react.js·前端框架
用户9714171814272 小时前
JavaScript call、apply、bind 详解
javascript
用户9714171814272 小时前
JavaScript 深拷贝与浅拷贝详解
javascript
Highcharts.js2 小时前
Highcharts React v4 迁移指南(上):核心变更解析与升级收益
前端·javascript·react.js·react·数据可视化·highcharts·v4迁移
SuniaWang2 小时前
《Spring AI + 大模型全栈实战》学习手册系列 · 专题八:《RAG 系统安全与权限管理:企业级数据保护方案》
java·前端·人工智能·spring boot·后端·spring·架构
菌菌的快乐生活2 小时前
在 WPS 中设置 “第一章”“第二章” 这类一级编号标题自动跳转至新页面
前端·javascript·wps