LeetCode: 146. LRU 缓存机制 | 刷题打卡

595 阅读4分钟

本文正在参与掘金团队号上线活动,点击 查看大厂春招职位

一、题目描述

运用你所掌握的数据结构,设计和实现一个LRU (最近最少使用) 缓存机制

实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组关键字 - 值。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间

进阶: 你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

二、思路分析

先说说 LRU 算法的概念吧!(题目中的连接也可以看看,就是关于 LRU 的百度百科)

LRULeast Recently Used 的缩写,即最近最少使用,该算法是一种内存管理算法。

这个算法的大概逻辑是:
假设,长期不被使用的数据,在后期被用到的概率相对较低。
因此,当数据的数量达到了一定的阈值的时候,我们可以移除掉最近最少被使用的数据

队列

那么从以上就很容易得知一个特性,那就是 先进先出 ,说到这个 先进先出 我们最先想到的数据结构就是队列 ,举个例子:

// 长度为 2
LRUCache lRUCache = new LRUCache(2);

// 添加一个元素
lRUCache.put(1, 1); | [{ key: 1, value: 1 }]

// 添加第二个元素
lRUCache.put(2, 2); | [{ key: 1, value: 1 }, { key: 2, value: 2 }]

// 添加第三个元素                           出队
lRUCache.put(3, 3); | { key: 1, value: 1 } <- [{ key: 2, value: 2 }, { key: 3, value: 3 }, ]

但是有个问题是,队列的 getput 的实现并不满足 O(1) 的时间复杂度

想要在 O(1) 的时间复杂度内完成这两种操作,我们就需要用到特殊的数据结构 哈希链表

哈希链表

什么是哈希链表呢?
简单的说,就是 哈希表 + 双向链表 的形式。

我们都知道 哈希表 是由若干个 键值对(key - value) 组成的。

 key     key     key
-----   -----   -----   
value   value   value   

但是 哈希表键值对 它是无序的,谁先来谁后来都是一样的。

所以,我们需要一个关系链把它们都串起来,让它们彼此之间不再是毫无关系的。

每个 键值对 它都有一个前驱 键值对 和 后继 键值对 就和双向链表一样。

 key  ->  key  ->  key
-----    -----    -----   
value <- value <- value  

三、AC 代码

/**
 * 链表节点
 * @param {number} key 
 * @param {number} value
 */
const Node = function (key, value) {
    this.key = key
    this.value = value
    this.pre = null // 前驱
    this.next = null // 后继
}

/**
 * LRU 缓存器
 * @param {number} capacity
 */
const LRUCache = function(capacity) {
    this.head = null // 链表头
    this.end = null // 链表尾
    this.capacity = capacity // 容量
    this.map = new Map() // 哈希表
};

/** 
 * 获取指定的记录
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function(key) {
    const d = this.map.get(key)
    
    // 判断是否存在,不存在返回 -1
    if(!d) return -1
    
    // 因为使用了,所有刷新节点
    this.refreshNode(d)
    
    return d.value
};

/** 
 * 添加新的记录
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function(key, value) {
    const d = this.map.get(key)
    
    // 当节点不存在的时候
    if(!d) {
        // 判断是否超出容量,如果超出容量了,就把头部的节点删除
        if(this.map.size >= this.capacity) {
            // 链表中删除节点并返回 key
            const oldKey = this.removeNode(this.head)
            
            // 哈希表中删除节点
            this.map.delete(oldKey)
        }
        
        // 添加新的记录(节点)
        const newNode = new Node(key, value)
        this.addNode(newNode)
        this.map.set(key, newNode)
    } 
    
    // 节点存在的时候,更新值,并且刷新节点
    else {
        d.value = value
        this.refreshNode(d) 
    }
};

/** 
 * 刷新节点
 * @param {Node} node 
 * @return {void}
 */
LRUCache.prototype.refreshNode = function(node) {
    // 当节点再末尾的时候,就不用管它了,已经是最新的了
    if(node === this.end) return
    
    // 否则删除掉节点,并重新记录
    this.removeNode(node)
    this.addNode(node)
};

/** 
 * 删除节点
 * @param {Node} node 
 * @return {any} key
 */
LRUCache.prototype.removeNode = function(node) {
    // 当只有一个节点的时候
    if(node === this.head && node === this.end) {
        this.head = null
        this.end = null
    } 
    
    // 当节点是尾部的时候
    else if (node === this.end) {
        this.end = this.end.pre
        this.end.next = null
    } 
    
    // 当节点是头部的时候
    else if (node === this.head) {
        this.head = this.head.next
        this.head.pre = null
    } 
    
    // 当节点是中间的时候
    else {
        node.pre.next = node.next
        node.next.pre = node.pre
    }
    
    // 返回被删除节点的 key
    return node.key
};

/** 
 * 添加节点
 * @param {Node} node 
 * @return {void}
 */
LRUCache.prototype.addNode = function(node) {
    // 当末尾节点存在的时候,设置添加节点的前驱是末尾节点,末尾节点的后继是添加节点
    if(this.end) {
        this.end.next = node
        node.pre = this.end
        node.next = null
    }
    
    // 更换末尾节点
    this.end = node
    
    // 当头部节点不存在的时候,将头部节点设置为新增节点
    if(!this.head) this.head = node
};

const lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
console.log(lRUCache.get(1));    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
console.log(lRUCache.get(2));    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
console.log(lRUCache.get(1));    // 返回 -1 (未找到)
console.log(lRUCache.get(3));    // 返回 3
console.log(lRUCache.get(4));    // 返回 4

四、总结

使用 队列 来做的话,相对来说比较简单,但是性能方面就会有些不尽人意

所有引出了 哈希链表 ,只要了解 哈希链表 这个数据结构的特点,那么就可以使用该数据结构来解决该问题,并且性能方面相对来说也比较好。

需要注意的就是 添加节点删除节点更新节点 的时候链表的结构,和 headend 的存储,以及及时删减 Map 当中的过时的数据。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情