发布于2026-07-19 阅读(0)
扫一扫,手机访问
先说结论:在C++里实现LRU缓存,最靠谱的方案,还是手写一个双向链表,搭配一个存着节点裸指针的哈希表。直接用std::list替代,看着很美,但实操起来,坑比想象中多。
std::list 不能直接替代自定义双向链表理论上,std::list::erase(iterator) 确实是 O(1) 的,问题在于,你得先拿到这个迭代器。而哈希表里存的是值,不是迭代器。想在std::list里通过值反查迭代器,那就得 O(N) 遍历了,这跟LRU的初衷背道而驰。
当然,有人会想到在哈希表里存std::list::iterator,配合splice来移动节点。C++11之后,std::list的迭代器是可拷贝的,这个方案技术上可行。但有个隐含风险:这个迭代器的生命周期,必须和它所指向的节点严格同步。一旦节点被erase,这个迭代器就立刻失效了,后面再拿它操作,就是未定义行为。
所以,更稳妥的做法,就是自己动手搞一个双向链表节点:
key、value、prev、next 四个字段。std::unordered_map,直接存节点的裸指针。get() 和 put() 中如何避免重复逻辑写的时候,最怕的就是get()里写一遍“把节点移到头部”,put()里再写一遍,一不留神就漏掉某个指针赋值,尤其是边界情况。
实操建议很直接:把“把某节点移到头部”这个操作,抽成一个独立的函数。比如moveToHead(Node* node),内部拆成两步:removeNode(node) 和 addToHead(node)。
注意,removeNode() 里必须判空:
if (node->prev) node->prev->next = node->next;node->next。而在addToHead()里,更新指针的顺序至关重要。得先更新 node->next = head->next,再更新 head->next->prev = node,最后把 head->next 指向node。顺序弄反了,链表就断了。
tail 节点删除的正确顺序删掉 tail 节点,绝对不是 delete tail 这么简单。它涉及三件事:从哈希表移除 key、从链表解链、释放内存。顺序错了,不是野指针就是内存泄漏。
标准流程,必须严格遵守:
std::unordered_map 里,用 erase(tail->key) 把映射关系摘除。removeNode(tail),把它从链表中摘下来。此时 tail 指针还是有效的,能取到里面的 key。delete tail。删完之后,立刻更新 tail 指针指向它的前一个节点,也就是 tail = tail->prev。千万记住,如果先 delete tail,再去 erase(map[tail->key]),那 tail->key 已经是野指针了,访问它就是未定义行为。
不用哨兵也能做,但每处插入和删除,都得加一堆判空逻辑:if (!head) { head = node; tail = node; }。代码膨胀不说,还特别容易出错。
引入两个固定哨兵节点(head 和 tail 永远不存真实数据),所有操作就变成了“在中间插入”或“在中间删除”,逻辑高度统一,边界情况全被吸收掉了。
关键细节:
head->next = tail,tail->prev = head,其他字段置空。head 和 tail 之间。get() 后调用 moveToHead(),完全不用管 head 是否为空。size_ 和 capacity 就行,不需要遍历链表算长度。哨兵不是炫技,它是把代码里所有可能出问题的边界 case 全部收归一处,少一个 if,就少一个 bug 滋生的温床。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8