1p3a-logo1p3a-logo
留学申请面试经验绿卡排期全民竞猜
APP
    旧版通行证登录注册APP
首页
热榜快讯
NEW
通知私信收藏阅帖历史积分中心
热门功能
💎每日夺宝🌱每日农场🐱领养宠物🛍️跳蚤市场🏠租房找室友🛒好物折扣💳信用卡助手📱旧机回收比价
我的版块
我的标签

[面经] NVIDIA Coding:LRU 磁盘空间管理器(按容量淘汰)高频题

冷目灼灼的扁豆
2026/10/20 · 发布于刷题版·568
[面经] NVIDIA Coding:LRU 磁盘空间管理器(按容量淘汰)
最近面了 NVIDIA,分享一下技术电面里的一道题。表面上看就是 LRU 缓存,但有个关键区别:容量限制的是总大小,不是条数。这一点同时影响淘汰逻辑和更新路径,很适合检验 LRU 的基本功到底是真的扎实还是背下来的。面试官一开始没有限定淘汰策略(合理的策略都可以,比如 LRU),后来为了让结果确定,固定用 LRU,操作也以批量形式给出、按顺序处理。
题目
实现一个内存里的磁盘空间管理器,总容量固定。数据集用 datasetId 标识,每个有一个整数 size。
  • put(datasetId, size):存入一个数据集。空闲空间不够时,淘汰最久未使用的数据集,直到能放下为止。如果 size > totalCapacity,这次 put 失败,并且状态必须保持原样。
  • get(datasetId):返回存的大小,不存在返回 -1。
细节规则:
  • 每次成功的 get 和每次成功的 put(包括更新)都让该数据集变为最近使用。
  • put 一个已存在的 datasetId 是替换它的大小。先释放旧大小,再决定要不要淘汰别人。
  • 为了可测试,操作以批量形式给出:solution(total_capacity, operations) 按顺序返回每个操作的结果。put 成功返回 "ok",失败返回 "error",get 返回大小或 -1。
约束:total_capacity 最大 1e9,操作数最多 2e5,size 在 1 到 1e9 之间,datasetId 是非空字符串。
例题走一遍
容量 10,操作序列:put A 4, put B 3, get A, put C 5, get B, get A, get C。
  1. put A (4):放得下。used = 4。新旧顺序:A
  2. put B (3):放得下。used = 7。顺序:A, B
  3. get A:返回 4,A 变为最近使用。顺序:B, A
  4. put C (5):空闲只有 3,不够。B 是最久未使用的,先淘汰 B(used = 4),再放入 C(used = 9)。顺序:A, C
  5. get B:-1,B 已经被淘汰了
  6. get A:4
  7. get C:5
输出:['ok', 'ok', 4, 'ok', -1, 4, 5]
最容易错的一步在第 4 步:被淘汰的是 B 不是 A,因为第 3 步的 get 刷新了 A。
核心思路
需要同时做到两件事:按 datasetId O(1) 查找,以及 O(1) 拿到最久未使用的数据集。这就是 LRU 缓存经典的哈希表 + 有序结构组合。和教科书版 LRU 缓存的区别有三点:
  • 淘汰是一个循环,不是一次弹出:一次 put 可能淘汰多个数据集。
  • used 的记账要经得起更新:先减掉旧大小再淘汰,否则会淘汰不必要的数据集,更糟的是可能把正在更新的数据集自己淘汰掉。
  • put 只有一种失败方式:size > capacity。只要它本身能放进总容量,把其他都淘汰掉总能腾出地方,没有第二种失败路径。
方法 A:OrderedDict
from collections import OrderedDict

class DiskSpaceManager:
def __init__(self, total_capacity):
self.capacity = total_capacity
self.used = 0
self.store = OrderedDict() # datasetId -> size, LRU first

def get(self, dataset_id):
if dataset_id not in self.store:
return -1
self.store.move_to_end(dataset_id) # now most recently used
return self.store[dataset_id]

def put(self, dataset_id, size):
if size > self.capacity:
return "error" # can never fit; state untouched
if dataset_id in self.store:
# Update: free the old size before evicting anyone else.
self.used -= self.store.pop(dataset_id)
while self.used + size > self.capacity:
_, evicted_size = self.store.popitem(last=False) # LRU end
self.used -= evicted_size
self.store[dataset_id] = size
self.used += size
return "ok"

def solution(total_capacity, operations):
mgr = DiskSpaceManager(total_capacity)
out = []
for op in operations:
if op[0] == "put":
out.append(mgr.put(op[1], op[2]))
else:
out.append(mgr.get(op[1]))
return out

方法 B:哈希表 + 双向链表
同样的设计,但不用库来维护顺序:一个从 datasetId 到节点的字典,再加一条带头尾哨兵的双向链表保存新旧顺序(靠近头的是最久未使用,靠近尾的是最近使用)。适合 Java 里没有 access-order LinkedHashMap 的情况,或者面试官想看链表本身实现的时候。
class _Node:
__slots__ = ("key", "size", "prev", "next")

def __init__(self, key=None, size=0):
self.key = key
self.size = size
self.prev = None
self.next = None

class DiskSpaceManagerDLL:
def __init__(self, total_capacity):
self.capacity = total_capacity
self.used = 0
self.map = {}
self.head = _Node() # sentinel: head.next is the LRU dataset
self.tail = _Node() # sentinel: tail.prev is the MRU dataset
self.head.next = self.tail
self.tail.prev = self.head

def _unlink(self, node):
node.prev.next = node.next
node.next.prev = node.prev

def _append_mru(self, node):
last = self.tail.prev
last.next = node
node.prev = last
node.next = self.tail
self.tail.prev = node

def get(self, dataset_id):
node = self.map.get(dataset_id)
if node is None:
return -1
self._unlink(node)
self._append_mru(node)
return node.size

def put(self, dataset_id, size):
if size > self.capacity:
return "error"
old = self.map.pop(dataset_id, None)
if old is not None:
self._unlink(old)
self.used -= old.size
while self.used + size > self.capacity:
lru = self.head.next
self._unlink(lru)
del self.map[lru.key]
self.used -= lru.size
node = _Node(dataset_id, size)
self.map[dataset_id] = node
self._append_mru(node)
self.used += size
return "ok"

复杂度
  • get:平均 O(1)。
  • put:平均 O(1) 加上淘汰的开销。每个数据集每次插入后最多被淘汰一次,所以 n 个操作总共是 O(n);淘汰循环是均摊的,不会变成每个 put 都二次方。
  • 空间:O(当前存着的数据集数量)。
易错点
  1. 失败的 put 什么都不能改。size > capacity 时不要淘汰、不要插入、不要动新旧顺序。更新的情况也一样:超大的更新要让旧数据集保留旧大小和旧位置。
  2. 先释放旧大小,再淘汰。更新时如果先淘汰后记账,会多淘汰不必要的数据集,还可能把正在更新的数据集自己淘汰掉。
  3. get 未命中不算一次使用。返回 -1 时不能创建条目,也不能改新旧顺序。
  4. 一旦新数据集能放下就停止淘汰。多淘汰一个是最常见的 off-by-one;上面的例题能抓到它(第 4 步如果连 A 一起淘汰,put 照样成功,但第 6 步就会错误地返回 -1)。
验证:除了上面的例题,还用 500 组随机操作序列和一个朴素 O(n) 参考实现做了对拍,两种方法全部一致。
觉得有帮助的话欢迎收藏、加米。也欢迎讨论变体,比如把 LRU 换成 LFU,或者给数据集加 TTL。
已获得 1 大米
avatar
5
共1条回复

✨ 您正在体验新版论坛UI

👉 【有奖公测】反馈问题或建议

新手指南常见Q&A小黑屋关于我们加入团队联系客服VIP通行证购买鳄梨去广告企业招聘地里专栏商务洽谈服务条款社区守则隐私政策
youtubetwitter
1Point3Acres.com does not represent or guarantee the truthfulness, accuracy, or reliability of any of communications posted by users.
Copyright ©2009-2026 1Point3Acres.com All rights reserved. See Terms of Service.