PHP 实现 LRU 缓存算法面试

3次阅读

lru缓存核心是淘汰最久未被访问元素,需哈希表+双向链表实现o(1)查找、移动和删除;php中推荐spldoublylinkedlist配合数组映射,但offsetunset等操作为o(n),高频场景应自定义链表节点。

PHP 实现 LRU 缓存算法面试

LRU 缓存的核心逻辑是什么?

LRU(Least Recently Used)要求在容量满时,淘汰**最久未被访问**的元素。关键不是“最早加入”,而是“最近一次访问时间最靠前”。所以必须同时支持:快速查找(O(1))快速移动到最新位置(O(1))快速删除最旧节点(O(1))。纯数组或普通哈希表无法同时满足——PHP 中最常用解法是 哈希表 + 双向链表组合。

PHP 中怎么高效实现双向链表结构?

不建议手写完整链表类。PHP 7.4+ 推荐用 SplDoublyLinkedList,它原生支持头尾增删、迭代、偏移定位,且内部是双向链表实现。但注意:SplDoublyLinkedList 的 offsetGet() 是 O(n),不能用来随机查节点;所以仍需搭配关联数组Array)做 key → node 映射。典型结构:

  • 一个 array $map:存储 key ⇒ SplDoublyLinkedList 节点(实际存的是 value,但需能反查 key)
  • 一个 SplDoublyLinkedList $list:按访问顺序维护节点,头部为最新,尾部为最旧
  • 每次 get():查 map → 找到节点 → 从 list 中 detach → push front;
  • 每次 put():若存在则更新 value 并 move to front;若不存在且满,则 pop back(最旧)并 unset map 对应 key,再 unshift 新节点

面试时容易被追问的关键细节

别只写骨架代码。面试官常盯住这几个点:

  • 如何保证 get/put 都是 O(1)? —— map 提供 O(1) 查找;SplDoublyLinkedList 的 unshift/pop/rewind/next 是均摊 O(1),detach 需要先定位,但我们可以不 detach,改用“删除后重建”或更稳妥的“手动维护指针”(即自定义链表节点类)
  • PHP 数组键名是否区分大小写?key 是字符串还是整型 —— LRU 缓存 key 应支持任意可哈希类型,实际中通常限定为 String/int,构造时可加 is_scalar($key) 校验
  • 并发安全吗? —— 单线程没问题;若提 Web 场景,可说明“PHP-FPM 每请求单进程,天然隔离”,无需加锁;swoole多线程环境才需考虑
  • 内存泄漏风险? —— 注意不要在节点里循环引用自身或闭包,避免 GC 无法回收;SplDoublyLinkedList 节点本身不含引用陷阱,相对安全

一个简洁可用的 PHP LRU 实现(带注释)

以下代码满足面试基本要求,无依赖,可直接运行:

立即学习PHP免费学习笔记(深入)”;

class LRUCache {     private array $map = [];     private SplDoublyLinkedList $list;     private int $capacity;      public function __construct(int $capacity) {         $this->capacity = $capacity;         $this->list = new SplDoublyLinkedList();         $this->list->setIteratorMode(SplDoublyLinkedList::IT_MODE_FIFO);     }      public function get(mixed $key): mixed {         if (!isset($this->map[$key])) return -1;                  // 把对应节点移到头部(最新)         $node = $this->map[$key];         $this->list->offsetUnset($this->findIndex($key)); // O(n),仅用于演示;真实高频场景建议自定义链表         $this->list->unshift($node);         $this->map[$key] = $node;         return $node['value'];     }      public function put(mixed $key, mixed $value): void {         if (isset($this->map[$key])) {             // 更新值并移到头部             $this->list->offsetUnset($this->findIndex($key));             $this->list->unshift(['key' => $key, 'value' => $value]);             $this->map[$key] = ['key' => $key, 'value' => $value];             return;         }          // 新增         if ($this->list->count() >= $this->capacity && $this->capacity > 0) {             // 删除尾部(最久未用)             $tail = $this->list->pop();             unset($this->map[$tail['key']]);         }         $this->list->unshift(['key' => $key, 'value' => $value]);         $this->map[$key] = ['key' => $key, 'value' => $value];     }      private function findIndex(mixed $key): int {         $i = 0;         foreach ($this->list as $node) {             if ($node['key'] === $key) return $i;             $i++;         }         return -1;     } }

注意:findIndex 是 O(n),仅用于教学清晰性。高分答案会主动指出这点,并给出优化方向:用自定义双向链表节点(含 prev/next),配合 map 存储节点引用,实现真正 O(1) 的 move-to-front。

text=ZqhQzanResources