4. 基础数据结构实现:链表节点定义、哈希表初始化、双向链表增删操作

LRU 缓存的核心是快速查找和有序淘汰。要同时实现 O(1) 的 get 和 put 操作,必须将 哈希表 与 双向链表 组合使用。本章将定义基础节点结构,初始化哈希表与链表,并实现双向链表的增删操作,为后续的淘汰逻辑打下基础。

4.1 数据结构设计总览

  • 双向链表:按访问时间排序。最近访问的节点在 头部,最久未访问的节点在 尾部(淘汰候选)。
  • 哈希表:key -> 链表节点指针 的映射。支持 O(1) 键值定位。
  • 组合关系:哈希表负责快速定位节点,链表负责维护淘汰顺序。
本节目标:定义 DLinkedNode 结构体、LRUCache 类骨架,并完成链表与哈希表的初始化。

4.2 链表节点定义

每个节点存储 key、value 以及前后指针 prev 和 next。使用 int 类型为例,实际工程中可使用泛型。

// C++ 示例:双向链表节点
struct DLinkedNode {
    int key;
    int value;
    DLinkedNode* prev;
    DLinkedNode* next;

    // 带参构造函数
    DLinkedNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
    // 默认构造函数(用于哨兵节点)
    DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
};
设计要点:节点包含 key 是因为当淘汰尾部节点时,需要通过节点中的 key 去哈希表中删除对应的条目。

4.3 哈希表初始化

哈希表选用 unordered_map,键为 int,值为 DLinkedNode*。初始化时分配链表节点池或使用动态内存。

class LRUCache {
private:
    int capacity_;                    // 最大容量
    int size_;                        // 当前节点数
    DLinkedNode* head_;               // 虚拟头节点
    DLinkedNode* tail_;               // 虚拟尾节点
    unordered_map<int, DLinkedNode*> cache_; // 哈希表

public:
    LRUCache(int capacity) : capacity_(capacity), size_(0) {
        // 初始化双向链表:创建两个哨兵节点
        head_ = new DLinkedNode();
        tail_ = new DLinkedNode();
        head_->next = tail_;
        tail_->prev = head_;
    }
    ~LRUCache() {
        // 析构时释放所有节点
        DLinkedNode* cur = head_;
        while (cur) {
            DLinkedNode* next = cur->next;
            delete cur;
            cur = next;
        }
    }
};
哨兵节点(dummy node)技巧:使用 head_ 和 tail_ 两个不存储实际数据的哨兵节点,可以消除链表插入/删除时的边界判断,使代码更简洁、不易出错。

4.4 双向链表增删操作

在 LRU 缓存中,链表操作主要有三种:头部插入、尾部删除、节点移除(任意位置)。后两者是基础,头部插入可视为移除后再插入头部。

操作 场景 时间复杂度 实现要点
addToHead(node) 插入新节点或访问已有节点后移至头部 O(1) 更新 node、head_、原首节点共 4 个指针
removeNode(node) 淘汰尾部节点时,先移除再删除 O(1) 将 node->prev 与 node->next 直接相连
removeTail() 淘汰最久未使用的节点 O(1) 调用 removeNode(tail_->prev) 并返回该节点
moveToHead(node) get 或 put 已有 key 时更新顺序 O(1) 先 removeNode 再 addToHead

4.5 核心函数代码实现

以下是双向链表四项关键操作的实现。注意指针修改顺序,避免丢失链表地址。

// 将节点添加到虚拟头节点之后(头部实际首节点)
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;
}

// 移除并返回尾部实际节点(删除前仍需 return 以获取 key)
DLinkedNode* removeTail() {
    DLinkedNode* node = tail_->prev;
    removeNode(node);
    return node;
}

// 将已有节点移动到头部
void moveToHead(DLinkedNode* node) {
    removeNode(node);
    addToHead(node);
}
重要检查点:在所有指针赋值前,确保 head_->next 和 tail_->prev 不为空(由哨兵节点保证)。调试时先画指针图,再写代码。

4.6 数据结构关系图

下面用内联 SVG 展示哈希表与双向链表的协同结构,以及节点间的连接关系。哈希表指向链表节点,链表节点通过 prev/next 串联,head_ 和 tail_ 作为哨兵节点位于两端,实际数据节点位于中间。

LRU 数据结构:哈希表 + 双向链表 哈希表 cache_ key: 1 key: 2 key: 3 key: 4 ... 双向链表(最近访问 → 最久未访问) head_ key=1 key=2 key=3 tail_ 最近访问(头部) 最久未访问(尾部) next 指针 prev 指针

4.7 数据结构关系说明

  1. 哈希表:键为缓存 key,值为指向链表节点的指针,实现 O(1) 查找。
  2. 双向链表:真实数据节点按访问时间顺序排列,head_ 侧为最近使用,tail_ 侧为最久未使用。
  3. 哨兵节点:head_ 和 tail_ 不存储数据,简化边界情况处理。
  4. 淘汰机制:当缓存达到容量上限,移除 tail_->prev 节点,并删除哈希表中对应的 key。
编码建议:在实现哈希表和链表时,先定义节点结构,再编写类的构造函数和析构函数,确保初始化与资源释放的正确性。使用哨兵节点后,addToHead 和 removeNode 无需检查链表是否为空,代码更健壮。
常见错误:在 addToHead 中,如果先修改 head_->next->prev 再修改 head_->next,容易丢失指针。标准顺序:先处理 node 的前后指针,再调整前后节点的指针。