浏览器标题切换
浏览器标题切换end

关于LRU问题的一些新思考

题目链接

146. LRU Cache

NC 93 设计LRU缓存结构

注意:LeetCode的测试数据会更多一点,用146优化一下代码

思路

访问操作get:

  1. 情况1:不存在 -> 返回-1

  2. 情况2:存在 -> 更新到头部+返回值

插入操作set/put(k,v):

  1. 情况1:容量已满 -> 删除尾部元素

  2. 情况2:容量未满

    • 存在:更新(位置到头部+删除原来的位置+修改元素值)

    • 不存在:在头部插入该元素

针对使用的数据结构的分析

目前没有一个容器可以同时具备查找和更新(增/删)操作都在 \(O(1)\) 的时间复杂度内的。

可使用的数据结构如下:

  1. 查找 \(O(1)\):哈希表 -> 使用unordered_map [无序+底层哈希表]

  2. 更新 \(O(1)\)

    • 头插/尾插 \(O(1)\):队列、链表(单向、双向)

    • 但是除此之外,我们还需要考虑到,如果更新操作是发生在中间结点,那么队列是需要 \(O(n)\) 的时间才能更新的。

综上所述,我们应该使用 一个双向链表 + 一个哈希表(存key以及双向链表结点的迭代器) 这两个数据结构来实现LRU。

  • 哈希表负责查询+根据key定位链表具体结点(以帮助更新操作)【unordered_map<int, list<pair<int, int>>::iterator> mp;

  • 双向链表负责维护元素插入的顺序+更新最近访问的数据到头部+缓存到达上限时删除尾部元素。【最近访问的放头部,最少访问的放尾部】 【list<pair<int, int>> L; 表示双向链表L的每个结点】

为什么不用队列

如果更新操作是发生在中间结点,那么队列是需要 \(O(n)\) 的时间才能更新的,不满足 \(O(1)\) 的时间要求。

为什么不用单向链表+普通哈希表

以下代码可以能过牛客,leetcode超时TLE【批量put然后再批量get情况下】,比如下列这种数据情况是会TLE的:

结合下面代码来看:

  1. 如果使用单向链表:在缓存容量达到最大时,如果往里面继续插入元素,需要删除最近最少使用的元素。单向链表因为只存储了头结点、当前结点数据、当前结点指向的下一个结点的原因,导致如果我需要删除最后一个结点的时候,我需要遍历整个链表找到倒数第二个结点,把它的next指针设置为NULL才可以,所以删除尾结点的时间复杂度为 \(O(n)\)。但是双向链表可以在 \(O(1)\)完成该操作。

    • 双向链表在这里的优点体现出来了:快速删除最后一个元素+方便在链表头部插入新元素【不需再开辟额外指针】+ 方便删除链表中的任意节点。
  2. 如果使用普通哈希,也就是定义为 unordered_map<int, int> mp,如下图所示,会导致list在删除元素时候的remove操作到达 \(O(n)\) l.remove(key); // *****!!! list如果没有指向删除的元素的迭代器,那么这样删除是需要O(n)的!!!

    • 关键原因:在 C++ 中,std::listeraseremove 操作的复杂度是 O(1),但需要提供一个指向要删除元素的迭代器。

      • 详细解释如下: list如果没有指向删除的元素的迭代器,并执行了l.remove(key),那么这样删除是需要O(n)的!!!结合第一点来看,如果哈希表里面只存取了key和value,那么查找上是方便了,但是当我们删除一个结点的时候,我们是无法单单通过这个key去定位到双向链表中的具体结点位置的,如果我们无法直接定位到具体结点位置,意味着我们删除还是需要遍历整个双向链表,时间复杂度就不会达到 \(O(1)\)。另外, std::listremove(key) 函数来删除具有特定值(也就是等于key)的所有元素,而不是删除key对应的value。如果要看下详细视频图分析,戳这里 主要看下这里面的图。

TLE代码:

#include<algorithm>
#include<iostream>
#include<list>
#include<unordered_map>

using namespace std;

class Solution {

    list<int> l;  // 使用 list<int> 存储值 双向链表
    unordered_map<int, int> mp;  // 存储键和链表中元素的位置

    int cap;

  public:
    Solution(int capacity) {
        cap = capacity;
    }

    int get(int key) {
        if (mp.find(key) == mp.end()) 
            return -1;

        // 将值移到链表头部表示最近使用
        moveToHead(key);
        // return l.front();
        return mp[key];
    }

    void set(int key, int value) {
        if (mp.find(key) != mp.end())  // 如果 key 存在,更新值并移动到头部
        {
    
            moveToHead(key);
            mp[key] = value;
        }
        else  // 如果 key 不存在
        {
            //插入新节点到头部
            l.push_front(key);

            // 更新哈希表中的映射关系
            mp[key] = value;

            if (mp.size() > cap)  // 如果容量超限,删除尾部节点
                removeTail();
        }
    }

  private:
    void moveToHead(int key) {
        // 将值移到链表头部
        l.remove(key); // *****!!! list如果没有指向删除的元素的迭代器,那么这样删除是需要O(n)的!!!

        l.push_front(key);
    }

    void removeTail() {
        // 删除链表尾部节点
        int tailKey = l.back();
        mp.erase(tailKey);
        l.pop_back();
    }

};

AC代码 [含结点的双向链表+含迭代器的哈希表]

这个代码牛客和LeetCode都可以AC。
list<int>Llist<pair<int, int>> L都是STL里面双向链表的定义。

list<pair<int, int>> L:表示双向链表L的每个结点

unordered_map<int, list<pair<int, int>>::iterator> mp;

class LRUCache {
//用链表存,链表头部是最近使用的,尾部是最后使用的,如果要删去,就直接把尾部删去就好
    list<pair<int, int>> L;//双向链表doubly linked list
 
    // 这个 unordered_map 存储了一个整数(键)和一个双向链表中元素的位置(通过迭代器表示)的映射关系。
    unordered_map<int, list<pair<int, int>>::iterator> mp;
	// 键key, 值pair<int,int>链表节点
	// mp[key]表示一个迭代器
	// *mp[key] key找到的迭代器所指向的值,即 pair<int, int>
	// 为什么这样定义:https://www.bilibili.com/video/BV1m34y1r7BC/?spm_id_from=333.337.search-card.all.click&vd_source=ec665d7e55ad181a7a6ebfccfac90052

    int cap;

public:
    LRUCache(int capacity) {
        cap = capacity;
    }
  
    // 访问
    // 情况1:不存在 -> 返回-1
    // 情况2:存在 -> 更新到头部+返回值
    int get(int key)
    {
        // ******* 情况1 *******
        // 检查键是否存在
        if (mp.find(key) == mp.end()) return -1; //  if(!mp[key])编译错误
        // if(mp.count(key)==0) return -1; // 键值的元素数量 //这两种写法都ok
 
        // ******* 情况2 ********
        auto tmp = *mp[key]; // 临时存储元素 代表原链表中的一个节点,也就是pair<int, int>
        //  tmp 的类型是 pair<int, int>。这是因为 mp[key] 返回一个迭代器,而 *mp[key] 解引用这个迭代器,得到了 pair<int, int> 类型的元素。具体地说,*mp[key] 返回的是 list<pair<int, int>>::iterator 类型的迭代器指向的元素,而这个元素是一个 pair<int, int>。所以,tmp 的类型就是 pair<int, int>。

        L.erase(mp[key]); // 删除key指向的结点 删除双向链表L中由 mp[key] 迭代器指向的节点。
        //map.erase(key);
        L.push_front(tmp);// 把该元素更新到头部
        mp[key] = L.begin();
        return L.front().second; //返回list头部元素对应的值【second属性】
    }
 
    // 插入
    // 情况1:容量已满 -> 删除尾部元素
    // 情况2:容量未满
    //      存在:更新(位置到头部+删除原来的位置+修改元素值)
    //      不存在:在头部插入该元素
 
    void put(int key, int value)
    {
        // // ******* 情况1 *******
        // if (cap == L.size())
        // {
        //     mp.erase(L.back().first); // 在map中 删除了链表最后一个元素的前驱
        //     L.pop_back(); // 删除了链表最后一个结点
        //     // return; 这里不管怎么样,都需要更新数据,进行下面的代码,所以不要在这里return

        //     // 问题在于,set可能是更新也可能是插入操作,不能在这里立马判断缓存满了就把最后一个元素删去。
        // }
 
        // ******* 情况2 *******
 
        if (mp.find(key) != mp.end()) // 存在 -> 更新
        {
            // mp[key] = L.begin();// mp[key]=value // 更新元素对应的值
            // L.push_front(*mp[key]); //放在头部
            L.erase(mp[key]); // 删除原来的元素[因为要更新的kv,相当于直接删除了原来的结点]
            L.push_front(make_pair(key, value));
      
            mp[key] = L.begin();
            return;
        }

        // 不存在 - 判断缓存是否已满 + 满了删除最后一个元素再头插/未满直接头插
        if (cap == L.size())
        {
            mp.erase(L.back().first); // 在map中 删除了链表最后一个元素的前驱
            L.pop_back(); // 删除了链表最后一个结点
        }
        L.push_front(make_pair(key, value));
            // L.push_front(pair<int, int>(key, value));
        mp[key] = L.begin(); //第一个迭代器
    }

};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

手写参考

https://leetcode.cn/problems/lru-cache/solutions/12711/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/
https://labuladong.gitee.io/algo/di-yi-zhan-da78c/shou-ba-sh-daeca/suan-fa-ji-fb527/#二、思路分析

posted @ 2024-01-04 04:12  抓水母的派大星  阅读(24)  评论(0编辑  收藏  举报