LeetCode Medium|【146. LRU 缓存】

力扣题目链接

题意:本题的题意就是希望我们设计一个满足 LRU 缓存的数据结构,LRU即最近最少使用。
需要我们实现 get 和 put 方法,即从缓存中获取值和设置缓存中值的方法。
还有一个约束条件就是缓存应当有容量限制,如果实现 put 方法的时候,没有空闲的空间的话,需要淘汰一个最久没有使用的 key
同时要求 get 和 put 的时间复杂度是 O(1)

其实关于 LRU 最类似的一种应用就是浏览器记录,随着我们打开的浏览器越来越多,浏览历史表就会越来越长,如果我们想要打开某个浏览页面,也会直接从缓存中读取,并且由于我们打开了历史记录中的某个浏览页面,它会成为最新的那条记录。

测试用例解读

可以直接看 B 站视频 :【大厂面试官让我手写LRU缓存,还好提前抱了佛脚,春招有希望了】(具体位置从 3:00开始)

首先我们一个一个解决上面提出的几个问题:

  • 首先关于我们要求的 get 查询方法,很直观的一个想法就是使用 map 来进行实现,不过他只能实现查询时间复杂度为 O(1),但是由于 map 本身是无序的,所以我们希望他能够有新旧顺序的信息。
  • 很直观的思路,我们每次新建一个键值对的时候,就把这个 key-value 放入一个链表的头,我们每次存入新的节点,我们就把其作为新的头。这样我们链表的头部永远都是那个最新的 key-value;链表的尾部就是最久未使用的键值对
  • 但是我们仍然有一个很重要的问题无法实现:如果我们查询了某个 key-value ,并且该节点在链表的中间位置,那么我们就不能及时得将该节点放到链表的头部。因为我们的 map 是以 key-value 来进行存取的,所以我们不能在链表中及时找到对应的节点
  • 为了应对上面的情况,有一个比较好的思路就是,当我们存储节点时,map 中的 key 就是该节点的键,map 中的 value 就是该节点所在链表的节点(ListNode*)。通过这样的方法,我们可以快速定位到链表节点,而不需要根据别的信息进行遍历。
  • 根据以上的要求,我们可以知道,使用单向链表是无法实现上述想法的,因为我们的节点是需要往前移动到链表头部,所以这里的数据结构使用双向链表。

总上所述,我们的代码雏形就出来了。

总体代码

  • 首先定义双向链表的节点结构:每个结构包括 key-value 的值和 prev 和 next 指针,并且定义两个构造函数
struct Node {
    int key, value;
    Node *prev, *next;
    Node() 
        : key(0), value(0), prev(nullptr), next(nullptr) {}
    Node(int key, int value) 
        : key(key), value(value), prev(nullptr), next(nullptr) {}
};

  • 下面来实现 LRU 缓存:定义链表的虚拟头、尾节点;哈希表来存储 key 和 双向链表节点 的映射关系;最后是我们的容量大小,以及当前已使用的大小。
class LRUCache {
private:
    std::unordered_map<int, Node*> hashMap_;
    int capacity_, size_;
    Node *dummyHead_, *dummyTail_;
};
  • 实现 LRUCache 的构造函数:
class LRUCache {
private:
	...
public: 
    LRUCache(int capacity) 
        : capacity_(capacity), size_(0) {
            dummyHead_ = new Node();
            dummyTail_ = new Node();
            dummyHead_->next = dummyTail_;
            dummyTail_->prev = dummyHead_;

        }
  • 接下来我们来实现从链表中删除节点和插入节点到链表头的方法,该方法是其中的 get 和 put 方法中的重要:
    void removeNode(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
	 // 在头节点处插入一个 node
    void addNodeToHead(Node* node) {
        node->prev = dummyHead_;
        node->next = dummyHead_->next;
        dummyHead_->next->prev = node;
        dummyHead_->next = node;
    }
  • 接下来实现重要的 get 方法:首先我们需要确定节点时候在哈希表中:
    int get(int key) {
        if (hashMap_.find(key) != hashMap_.end()) {
            Node* node = hashMap_[key];
            removeNode(node);
            addNodeToHead(node);
            return node->value;
        }
        return -1;
    }
  • 随后是设置节点的值:如果该节点在哈希表中存在的话,我们就重新设置其节点的值,随后更新其位置在最前面;如果不存在的话,说明要插入一个新的节点,我们首先要判断一下容量,如果容量达到了上限,我们就需要从链表的尾部淘汰一个节点,然后在进行插入
    void put(int key, int value) {
        if (hashMap_.find(key) != hashMap_.end()) {
            Node* node = hashMap_[key];
            node->value = value;
            removeNode(node);
            addNodeToHead(node);
        } else {
            if (size_ == capacity_) {
                Node* removed = dummyTail_->prev;
                hashMap_.erase(removed->key);
                removeNode(removed);
                delete removed;
                size_--;
            }
            Node* node = new Node(key, value);
		    addNodeToHead(node);
		    hashMap_[key] = node;
		    size_++;
        }
    }

简洁实现

这里介绍一个简介实现,如下:

class LRUCache {
public:
    LRUCache(int capacity) : capacity_(capacity) {}

    int get(int key) {
        auto it = cacheMap.find(key);
        if (it == cacheMap.end()) {
            return -1; // Key not found
        } else {
            // Move the accessed (key, value) pair to the front of the cacheList
            cacheList.splice(cacheList.begin(), cacheList, it->second);
            return it->second->second;
        }
    }

    void put(int key, int value) {
        auto it = cacheMap.find(key);
        if (it != cacheMap.end()) {
            // Key already exists, update the value and move it to the front
            it->second->second = value;
            cacheList.splice(cacheList.begin(), cacheList, it->second);
        } else {
            if (cacheList.size() == capacity_) {
                // Cache is full, remove the least recently used item
                auto last = cacheList.back();
                cacheMap.erase(last.first);
                cacheList.pop_back();
            }
            // Insert the new key-value pair at the front
            cacheList.emplace_front(key, value);
            cacheMap[key] = cacheList.begin();
        }
    }

private:
    int capacity_;
    std::list<std::pair<int, int>> cacheList; // Stores the (key, value) pairs
    std::unordered_map<int, std::list<std::pair<int, int>>::iterator> cacheMap; // Maps key to the corresponding iterator in cacheList
};

类成员变量

首先定义一个类成员变量:

class LRUCache {
private:
    int capacity_;
    std::list<std::pair<int, int>> cacheList_; // Stores the (key, value) pairs
    std::unordered_map<int, std::list<std::pair<int, int>>::iterator> cacheMap_; // Maps key to the corresponding iterator in cacheList
};

这里的 cacheList 即我们之前所维护的那个双向链表;
cacheMap 就是我们之前维护的那个 hashMap ,key 是键值, value 是我们之前的链表节点。

在此之前,我们自己定义个用于缓存的节点,但是我们可以直接使用 std::pair<int, int> 来代替我们自己构造的类;

除此之外:std::pair<int, int>>::iterator 是一个类型声明,用于表示指向 std::list<std::pair<int, int>> 中元素的迭代器,这个迭代器类型可以用来遍历或访问 std::list 容器中的元素。

接下来我们开始进行主要成员方法的实现:

构造函数

class LRUCache {
public:
    LRUCache(int capacity) : capacity_(capacity) {}
private:
	...
};

get 方法

    int get(int key) {
        //这里 it 的类型是 std::unordered_map<int, std::list<std::pair<int, int>>::iterator>::iterator
        auto it = cacheMap_.find(key);
        if (it == cacheMap_.end()) {
            return -1;
        } else {
            // 将键值对移动到链表的最前面,it->second 指向的是it对应的那个键值对
            cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
            //这里返回的是对应的 value
            return it->second->second;
        }
    }

put 方法

    void put(int key, int value) {
        auto it = cacheMap_.find(key);
        if (it != cacheMap_.end()) {
            it->second->second = value;
            cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
        } else {
            // 如果已经到了最大容量,我们应该删除最久未使用,然后把当前该键值对放在最前面
            if (cacheList_.size() == capacity) {
                auto last = cacheList_.back();
                cacheMap_.erase(last.first);
                cacheList_.pop_back();
            }
            //放到最前面
            cacheList_.emplace_front(key, value); //直接原地构造 key-value
            cacheMap_[key] = cacheList_.begin(); //记录该键值对的迭代器
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值