LRU 缓存

这题难死了。另外我完全放弃非口语化描述。我日常怎么说话笔记就怎么写了。方便自己理解。上次写这题是 4 月 28 日,记得代码行数很多,现在的话,重写没有任何勇气。目前是写了 38 题了。每天至多写一个新题,可以不断复习之前写过的题,也就是前面 38 题。感觉前面的题,都写不了了。反正目标就是写 100 题,多的题,一个都不写。反正。。。算了。菜是我的原罪。我可以看自己写的笔记来复习写过的算法题。这是自己的独家复习资料。自己用起来最称手。看了以下是 66 行代码,吓哭了。华语女歌手精选这个歌单好听。

  1. key 和 val 是结构体 node 里面的两个参数,表示键和值。感觉和哈希表有点像。但是不是一个东西吧。因为哈希表是常数时间访问的容器。
  2. 结构体里面定义了两个指针,这是一个新的操作,实际上也非常常见,就是双向链表。感觉主要是要学这种写法。结构体后面要加一个分号。结构体一般的定义后面,也就是把参数设置为默认值的那一行,不需要加分号。设置参数的行,括号里面表示两个传入的参数,随便写,但是需要定义正确类型,括号后面要有冒号,指针不需要加 *
  3. 定义好几个结构体里面的参数就可以了。
  4. capacity & size 是干啥的。我有点忘记了。capacity 应该是 cache 的容量,size 应该是实际的大小,应该就是 size 超过 capacity 就需要换掉。点题,LRU 就是因为不够缓存,就把最近最久未使用的换出去。least recently used 。
  5. *head *tail 都是用的我们前面定义的那个结构体。等我写完这个题就去休息。我废了。
  6. *head *tail 其实是虚拟节点。奥也不是节点?应该是节点。就是最开始肯定是 *head 指向 *tail ,*tail 指向 *head ,然后往中间加东西。*head 和 *tail 不存实际的东西。和那个 dummy 很类似。孙燕姿的 180 度真好听啊。
  7. 定义了一个哈希表,哈希表用 cache 表示,即缓存。参数是 int 和 Node* ,也就是说,可以通过 int 常数时间找到 Node*, 有点不理解。这是啥呢。奥。这是模拟 cache 的高速,就是我们访存比较慢速,但是我们访问 cache 是非常快的,几乎可以认为是常数时间。哈希表明显就是常数时间可以通过键找到所谓的值。这里的键就是这里的 int ,值就是这里的 Node*.
  8. 下面是四个可怕的辅助函数。实际上每个函数也就几行。主要是多。复杂的工程就是简单的东西拼凑起来的,简单的东西多了,整个工程就难了。很有哲理的。
  9. 删除有点意思。通俗地说,没有实际地删除 node 这个节点,只是修改了指针,没有实际地 delete node ,当然从释放内存角度考虑,最后肯定还是要释放这个节点的内存的。所以我们的 public 里面是有 delete 这个操作的。把节点的前面的节点的 next 指针指向节点的后面一个节点,节点的后面一个节点的 prev 指针指向节点的前面一个节点,等效删除。
  10. 下一个函数是添加到头节点。我自己想以下哈。首先是,如果头节点是这个节点就直接结束就行。如果不是呢。但是这一行我不会写。我不知道 head 指向的是啥。实际上应该还有一点是,node 的 next 指向 head 的 next ,head 的 next 的 prev 指向 node ,然后 head 的 next 指向 node 应该就可以了。但是这里只有三行?我好像不小心看到有四行代码了。肯定就是我不会写的那行占领了一行?
  11. 我傻逼了。是少了 node 的 prev 的指向。
  12. 删除到头节点,我没看懂是啥意思,仔细看了代码,做的操作是删除节点,然后把这个节点添加到头节点,实际上就是说,访问了一次这个节点吧。如果在链表头部,认为是最近使用过了。很多东西就是要使用才灵活,比如说数学题算法题。最近最久未使用是每次都淘汰尾部的节点。最近使用并且提升到头节点的,肯定是最后才被删除的。我感觉自己理解得差不多了。
  13. 这里的删除尾部元素,使用了 *tail 这个哨兵,*tail 不是实际的尾部,*tail 前面的才是,双向链表可以解决这个问题。然后的话,我们定义的 remove 函数只修改了指针,并没有对内存进行释放。所以我们可以直接调用这个 remove 函数修改指针,然后返回 res 表示实际的尾部节点。public 里面释放 res 的内存即可。就真的删除了这个尾部节点,修改指针 + 释放内存。
  14. this->capacity 是为了区分变量和传进来的参数。有点麻烦。有点费解。实际上可以把传进来的参数改一下。或者把变量名改一下。但是有点容易引起歧义。所以没改。就这样吧。
  15. this->size 是冗余的,size 就完全足够,并没有和任何参数冲突。
  16. 为啥 private 里面定义 head 和 tail 需要定义Node *head, *tail,不可以直接定义为Node head, tail吗?* 表示的是指针,加了指针表示存的是一个地址,不加指针表示存的是一个对象。
  17. head 里面存的是地址,*head 表示的是具体的对象。new 出来的是一个地址,地址赋值给 head 就是把地址存到 head 里面,*head 就可以得到这个地址的对象。
  18. head 是地址,所以可以直接写 head->next 和 tail->prev
  19. 如果对着 key 找,找不到,也就是找到了 cache 的 end 位置,就表示 cache 里面没有这个节点,那么就是查找失败了。就返回 -1 表示失败。
  20. Node *node = cache[key] node 表示地址,*node 就是解地址,表示这个地址对应的对象。cache[key] 表示的是 Node* 本质上是一个地址。
  21. 如果这个节点 get 到了就把这个节点添加到链表首部,优先级提高,然后返回这个节点的值。
  22. 更新的函数,如果找到了这个节点,就更新数值并且提升优先级,也就是把节点提升到链表首部。
  23. 没找到这个节点就新建一个节点,键和值都是我们给的。然后,把哈希表里面的 key 和 newNode 对应起来,这样可以建立一个哈希表,常数时间实现查询操作。把新添加的节点添加到头部,也就是优先级最高,最近刚使用过的。
  24. 增加 size 表示现在 cache 里面的节点变多了。cache 里面的 key 和 Node* 是一一对应的,Node 里面有 key and value ,Node 里面的 key 和 cache 里面的 key 是一个 key
  25. 豆包现在反应速度好慢。不知道是我电脑慢还是他慢。我服了。
  26. 当 size 大于 capacity 的时候,就是我们需要删除链表尾部节点的时候。这个时候,我们直接取出来尾部的节点,delete 就可以了。当然调用函数的时候修改了指针。
  27. cache.erase(tailNode->key) 不错,就是反向删除了。把节点的 key 找到,因为 cache 和 Node 的 key 是同一个。
  28. 删除之后减小规模就可以了。挺难的。我觉得值得一个困难。哈哈哈。
  29. struct 定义参数那一行最后面还有 {}
  30. 实际运行好多小错误。难死我了。
  31. 写了 63 行,力竭了。。。
c 复制代码
class LRUCache {
public:
    LRUCache(int capacity) {
        this->capacity = capacity;
        this->size = 0;
        head = new Node( 0, 0 );
        tail = new Node( 0, 0 );
        head->next = tail;
        tail->prev = head;
    }
    int get(int key) {
        if ( cache.find( key ) == cache.end() ) return -1;
        Node *node = cache[key];
        moveToHead( node );
        return node->val;
    }
    void put(int key, int value) {
        if ( cache.find( key ) != cache.end() ) {
            Node *node = cache[key];
            node->val = value;
            moveToHead( node );
        } else {
            Node *newNode = new Node( key, value );
            cache[key] = newNode;
            addToHead( newNode );
            size++;
            if ( size > capacity ) {
                Node *tailNode = removeTail();
                cache.erase( tailNode->key );
                delete tailNode;
                size--;
            }
        }
    }
private:
    struct Node {
        int key, val;
        Node *next, *prev;
        Node( int k, int v ) : key( k ), val( v ), next( nullptr ), prev( nullptr ) {}
    };
    int capacity, size;
    Node *head, *tail;
    unordered_map<int, Node*> cache;
    void remove( Node *node ) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
    void addToHead( Node *node ) {
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
        node->prev = head;
    }
    void moveToHead( Node* node ) {
        remove( node );
        addToHead( node );
    }
    Node *removeTail() {
        Node *res = tail->prev;
        remove( res );
        return res;
    }
};
相关推荐
旷世奇才李先生5 小时前
Redis 7\.0实战:分布式缓存与高可用集群搭建全指南
redis·分布式·缓存
洛水水5 小时前
Redis 协议与异步通信深度解析
数据库·redis·缓存
后端漫漫20 小时前
Redis 客户端工具体系
数据库·redis·缓存
追梦开发者1 天前
Redis 避坑指南①:从安装到连接,这 9 个坑 90% 的人都踩过
redis·缓存·database
何中应1 天前
Redis集群搭建
数据库·redis·缓存
我是唐青枫1 天前
别只会用 MemoryCache!C#.NET CacheManager 详解:多级缓存、Region 与 Redis 实战
缓存·c#·.net
Lyyaoo.2 天前
Redisson
数据库·缓存
倒霉蛋小马2 天前
【Redis】什么是缓存击穿?
数据库·redis·缓存