跳到主要内容

146. LRU 缓存

题目描述

设计和实现一个 LRU (最近最少使用) 缓存机制。

实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value;如果不存在,则插入该组 key-value。当插入操作导致关键字数量超过 capacity,则应该 逐出 最久未使用的关键字。
  • 缓存中的所有 key 都是唯一的
  • 函数 getput 必须以 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

提示

  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 10^5
  • 最多调用 2 * 10^5getput

出处LeetCode

解法一:哈希表 + 双向链表

思路

我们可以使用哈希表和双向链表来实现 LRU 缓存机制。

  • 哈希表用于存储 keyvalue,并且可以通过 key 快速定位到对应的节点。
  • 双向链表用于存储 keyvalue,并且可以通过节点快速定位到前驱和后继节点。

我们可以使用一个伪头节点和一个伪尾节点来简化操作,伪头节点的后继节点是链表的头节点,伪尾节点的前驱节点是链表的尾节点。

实现

class LinkListNode {
key: number;
value: number;
prev: LinkListNode | null = null;
next: LinkListNode | null = null;

constructor(key = 0, value = 0) {
this.key = key;
this.value = value;
}
}

class LRUCache {
private capacity: number;
private cache: Map<number, LinkListNode> = new Map();
private head: LinkListNode = new LinkListNode();
private tail: LinkListNode = new LinkListNode();

constructor(capacity: number) {
this.capacity = capacity;
this.head.next = this.tail;
this.tail.prev = this.head;
}

get(key: number): number {
if (!this.cache.has(key)) {
return -1;
}

const node = this.cache.get(key)!;
this.moveToHead(node);

return node.value;
}

put(key: number, value: number): void {
if (!this.cache.has(key)) {
const node = new LinkListNode(key, value);
this.cache.set(key, node);
this.addToHead(node);

if (this.cache.size > this.capacity) {
const removed = this.removeTail();
this.cache.delete(removed.key);
}
} else {
const node = this.cache.get(key)!;
node.value = value;
this.moveToHead(node);
}
}

private addToHead(node: LinkListNode): void {
node.prev = this.head;
node.next = this.head.next;
this.head.next!.prev = node;
this.head.next = node;
}

private removeNode(node: LinkListNode): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}

private moveToHead(node: LinkListNode): void {
this.removeNode(node);
this.addToHead(node);
}

private removeTail(): LinkListNode {
const node = this.tail.prev!;
this.removeNode(node);

return node;
}
}

复杂度分析

  • 时间复杂度:O(1),对于 getput 操作,我们只需要在哈希表中查找节点,然后在双向链表中移动节点。
  • 空间复杂度:O(N),其中 N 是缓存的容量。