力扣精选题——LRU缓存

题目介绍

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
    函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

题目链接

题解

  • 分析上面的操作过程

    • 要让 put 和 get 方法的时间复杂度为 O(1)
    • 可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分
  • 显然 cache 必须有顺序之分

    • 以区分最近使用的和久未使用的数据
  • 而且我们要在 cache 中查找键是否已存在

  • 如果容量满了要删除最后一个数据

  • 每次访问还要把数据插入到队头

  • 那么

  • 什么数据结构同时符合上述条件呢?

    • 哈希表查找快,但是数据无固定顺序
    • 链表有顺序之分,插入删除快,但是查找慢
  • 所以结合一下

    • 形成一种新的数据结构:哈希链表。
  • LRU 缓存算法的核心数据结构就是哈希链表

    • 双向链表和哈希表的结合体
    • 这个数据结构长这样:
      在这里插入图片描述
      这里要感谢一下labuladong博主,我第一次接触算法是两年前的时候,越往深处走就越抽象,最后卡在了dp,一直不得要领,后来偶然看到了一篇题解,觉得讲的真好啊,然后就了解到了labuladong,真的很赞!

回归正题。

  • 为什么要是双向链表,单链表行不行?
  • 另外,既然哈希表中已经存了 key,为什么链表中还要存键值对呢,只存值不就行了?
  • 首先手写实现双端链表
class LRUCache {
	class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        // 构造函数,传参为空这个,是用来构造head和tail的
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {
            key = _key;
            value = _value;
        }
    }
    private DLinkedNode head, tail;
    private int size;
    private int capacity;
	
	public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
    private void addToHead(DLinkedNode node) {
        // 四步,因为是双端链表
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    private void moveToHead(DLinkedNode node) {
        // 两步操作
        //      删除该节点
        //      将该节点加入到head后面
        removeNode(node);
        addToHead(node);
    }
    // 删除并返回该节点
    private DLinkedNode removeTail(){
        // 找到tail节点前指针指向的节点
        //      移除掉他
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
    private void removeNode(DLinkedNode node) {
        // 这里只修改了两个指向,不过已经够了
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
}
  • 简单说一下:

    • LRUCache这个数据结构就是一个双端链表
    • 它下面的四个方法操作无非就是修改节点的前后指针
  • 那哈希表在哪儿用到了?

    • 在查找的时候用到了。
	private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
	public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null)
            return -1;
        // 如果key存在,先通过哈希表定位,在移动到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果key不存在,创建一个新节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // 如果key存在,先通过哈希表定位,在修改value,并移动到头部
            node.value = value;
            moveToHead(node);
        }
    }
  • 可以看到
    • cache的数据结构作用就在于查找
    • 能够一下就查找到key对应的DLinkedNode节点

回答刚才“为什么必须要用双向链表”的问题了

  • 因为我们需要删除操作
    • 删除一个节点不光要得到该节点本身的指针
    • 也需要操作其前驱节点的指针
      • 而双向链表才能支持直接查找前驱
      • 保证操作的时间复杂度 O(1)

最后回答之前的问答题“为什么要在链表中同时存储 key 和 val,而不是只存储 val”
注意这段代码:

if (size > capacity) {
    // 如果超出容量,删除双向链表的尾节点
    DLinkedNode tail = removeTail();
    // 删除哈希表中对应的项
    cache.remove(tail.key);
    --size;
}
  • 当缓存容量已满
    • 我们不仅仅要删除最后一个 Node 节点
    • 还要把 map 中映射到该节点的 key 同时删除
    • 而这个 key 只能由 Node 得到
      • 如果 Node 结构中只存储 val
      • 那么我们就无法得知 key 是什么
      • 就无法删除 map 中的键,造成错误
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值