前言
中等 ⚪ 这个题原本打算用双链表+最小堆做,发现无解。没想到双向链表。
题目
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 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 <= 105
- 最多调用
2 * 105
次get
和put
思路
设计一个哈希表用key为键,val和调用次数为值。同时用最小堆维护调用次数。
我的题解
错误解法
class LRUCache {private:int cap;unordered_map<int, pair<int, int>> LRUmap;priority_queue<pair<int, int>, // 存储 {timestamp, key}vector<pair<int, int>>, // 底层容器greater<pair<int, int>>> minHeap; // 最小堆(默认是最大堆,用 greater 反转)public:LRUCache(int capacity) {cap = capacity;}int get(int key) {auto it = LRUmap.find(key);if (it != LRUmap.end()){LRUmap[key].second++;return LRUmap[key].first;}else{return -1;} }void put(int key, int value) {// 判断是否存在// 不存在 是否超出容量// 超出容量 先逐出再进入auto it = LRUmap.find(key);if (it != LRUmap.end()){LRUmap[key].first = value;}else if (LRUmap.size() >= cap){cout<<minHeap.top().second<<endl;LRUmap.erase(minHeap.top().second);minHeap.pop();LRUmap[key] = {value, 0};minHeap.push({0, key});}else{LRUmap[key] = {value, 0};minHeap.push({0, key});}}
};/*** Your LRUCache object will be instantiated and called as such:* LRUCache* obj = new LRUCache(capacity);* int param_1 = obj->get(key);* obj->put(key,value);*/
官方题解
实现本题的两种操作,需要用到一个哈希表和一个双向链表。在面试中,面试官一般会期望读者能够自己实现一个简单的双向链表,而不是使用语言自带的、封装好的数据结构。在 Python 语言中,有一种结合了哈希表与双向链表的数据结构 OrderedDict,只需要短短的几行代码就可以完成本题。在 Java 语言中,同样有类似的数据结构 LinkedHashMap。
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:
对于 get 操作,首先判断 key 是否存在:
如果 key 不存在,则返回 −1;
如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
对于 put 操作,首先判断 key 是否存在:
如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。
上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1) 时间内完成。
小贴士
在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
struct DLinkedNode {int key, value;DLinkedNode* prev;DLinkedNode* next;DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};class LRUCache {
private:unordered_map<int, DLinkedNode*> cache;DLinkedNode* head;DLinkedNode* tail;int size;int capacity;public:LRUCache(int _capacity): capacity(_capacity), size(0) {// 使用伪头部和伪尾部节点head = new DLinkedNode();tail = new DLinkedNode();head->next = tail;tail->prev = head;}int get(int key) {if (!cache.count(key)) {return -1;}// 如果 key 存在,先通过哈希表定位,再移到头部DLinkedNode* node = cache[key];moveToHead(node);return node->value;}void put(int key, int value) {if (!cache.count(key)) {// 如果 key 不存在,创建一个新的节点DLinkedNode* node = new DLinkedNode(key, value);// 添加进哈希表cache[key] = node;// 添加至双向链表的头部addToHead(node);++size;if (size > capacity) {// 如果超出容量,删除双向链表的尾部节点DLinkedNode* removed = removeTail();// 删除哈希表中对应的项cache.erase(removed->key);// 防止内存泄漏delete removed;--size;}}else {// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部DLinkedNode* node = cache[key];node->value = value;moveToHead(node);}}void addToHead(DLinkedNode* node) {node->prev = head;node->next = head->next;head->next->prev = node;head->next = node;}void removeNode(DLinkedNode* node) {node->prev->next = node->next;node->next->prev = node->prev;}void moveToHead(DLinkedNode* node) {removeNode(node);addToHead(node);}DLinkedNode* removeTail() {DLinkedNode* node = tail->prev;removeNode(node);return node;}
};
封装对象知识点
在 C++、Python 和 Go 中封装对象(面向对象编程)的方式有所不同,各有其特点和语法。以下是三种语言中对象封装的核心方法对比:
---
## **1. C++ 对象封装**
### **核心特性**
• **类(Class)** 是封装的基本单位
• 支持 **访问控制**(`public`/`private`/`protected`)
• 显式定义构造函数/析构函数
• 支持运算符重载、友元函数等高级特性
### **示例代码**
```cpp
class Person {
private:
string name; // 私有成员
int age;
public:
// 构造函数
Person(string n, int a) : name(n), age(a) {}
// 公有方法
void introduce() {
cout << "I'm " << name << ", " << age << " years old." << endl;
}
// Getter/Setter
void setName(string n) { name = n; }
string getName() { return name; }
};
// 使用
Person p("Alice", 25);
p.introduce();
```
### **关键点**
• 用 `private` 隐藏数据,`public` 暴露接口
• 手动管理内存(或使用智能指针)
---
## **2. Python 对象封装**
### **核心特性**
• 基于 **类(Class)**,但所有成员默认 **公开**
• 通过命名约定(如 `_name`)表示私有性(非强制)
• 动态特性(可运行时添加成员)
• 支持装饰器(如 `@property`)
### **示例代码**
```python
class Person:
def __init__(self, name, age):
self._name = name # 约定为"私有"(实际仍可访问)
self.age = age # 公开成员
def introduce(self):
print(f"I'm {self._name}, {self.age} years old.")
# Getter/Setter(通过@property装饰器)
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
# 使用
p = Person("Bob", 30)
p.introduce()
p.name = "Charlie" # 调用setter
```
### **关键点**
• 私有性靠约定(如 `_name`),非语言强制
• 动态绑定(可随时添加/修改成员)
---
## **3. Go 对象封装**
### **核心特性**
• 通过 **结构体(Struct)** + **方法(Method)** 实现封装
• 无类概念,但可为类型绑定方法
• 通过 **首字母大小写** 控制可见性(跨包访问)
• 大写开头:`Name`(公开)
• 小写开头:`age`(包内私有)
### **示例代码**
```go
package main
import "fmt"
type Person struct {
name string // 小写开头,包内私有
Age int // 大写开头,公开
}
// 绑定方法(值接收者)
func (p Person) Introduce() {
fmt.Printf("I'm %s, %d years old.\n", p.name, p.Age)
}
// Getter(Go习惯直接访问字段,必要时用方法)
func (p *Person) Name() string {
return p.name
}
// Setter
func (p *Person) SetName(name string) {
p.name = name
}
func main() {
p := Person{name: "Dave", Age: 35}
p.Introduce()
p.SetName("Eve")
fmt.Println("New name:", p.Name())
}
```
### **关键点**
• 封装通过 **大小写命名** 实现
• 方法绑定到类型(非类)
• 无继承,通过 **接口(Interface)** 实现多态
---
## **对比总结**
| 特性 | C++ | Python | Go |
|---------------------|--------------------------|--------------------------|-------------------------|
| **封装单位** | 类(Class) | 类(Class) | 结构体(Struct)+ 方法 |
| **访问控制** | `public`/`private` | 命名约定(如 `_name`) | 首字母大小写(`Name` vs `age`) |
| **构造/析构** | 显式定义 | `__init__` | 工厂函数(如 `NewPerson()`) |
| **动态性** | 低(需编译) | 高(运行时修改) | 低(静态类型) |
| **内存管理** | 手动/智能指针 | 垃圾回收 | 垃圾回收 |
| **多态** | 虚函数/继承 | 鸭子类型 | 接口(Interface) |
---
## **何时选择哪种语言?**
• **C++**:需要高性能、精细控制内存或硬件交互
• **Python**:快速开发、动态需求或脚本场景
• **Go**:高并发服务、简洁的语法和跨平台部署
根据项目需求选择最合适的封装方式!
心得
这个题目一开始就想错了,是逐出“最久没调用”的,而不是调用次数最小的,这是不同的概念。故不能用最小堆,只能用链表。还没仔细看怎么实现,打算明天手写一下。
重新做已ac,重点在于要想到怎么设计这个数据结构:双向链表的节点,构建链表,公共变量。get、put、链表的删除新增操作都比较好写。这里体现出架构师的重要性....不要只当把自然语言翻译成编程语言的农民工啊!!!(甚至农民工都当不上,gpt已取代)