面试遇到算法题:实现LRU缓存

简介: V哥的这个实现的关键在于维护一个双向链表,它可以帮助我们快速地访问、更新和删除最近最少使用的节点,同时使用哈希表来提供快速的查找能力。这样,我们就可以在 O(1) 的时间复杂度内完成所有的缓存操作。哈哈干净利索,回答完毕。

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

这是一道大厂面试高频出现的算法题,难度为⭐️⭐️⭐️,属于中等,老铁们来一起看看这个题该怎么解?

1. 原题再现

没有废话,翠花,上酸菜!

2. 具体实现

为了实现一个满足 LRU (最近最少使用)缓存约束的数据结构,我们需要一个能够快速访问、更新和删除的数据结构,V 哥想到了用哈希表,因为哈希表提供了快速的查找能力,但是它不能保持元素的顺序;而双向链表可以保持元素的顺序,并且可以在 O(1) 时间复杂度内进行插入和删除操作。因此, V哥采用结合使用哈希表和双向链表来实现LRU缓存。

以下是 Java 代码实现:

import java.util.HashMap;

class Node {
   int key;
   int value;
   Node prev;
   Node next;

   public Node(int key, int value) {
       this.key = key;
       this.value = value;
   }
}

public class LRUCache {
   private HashMap<Integer, Node> cache;
   private Node head, tail;
   private int capacity;
   private int count;

   public LRUCache(int capacity) {
       this.capacity = capacity;
       this.cache = new HashMap<>();
       this.head = new Node(0, 0);
       this.tail = new Node(0, 0);
       this.head.next = this.tail;
       this.tail.prev = this.head;
       this.count = 0;
   }

   public int get(int key) {
       Node node = cache.get(key);
       if (node == null) {
           return -1;
       }
       moveToHead(node);
       return node.value;
   }

   public void put(int key, int value) {
       Node node = cache.get(key);
       if (node == null) {
           Node newNode = new Node(key, value);
           cache.put(key, newNode);
           addNode(newNode);
           count++;
           if (count > capacity) {
               Node toDel = popTail();
               cache.remove(toDel.key);
               count--;
           }
       } else {
           node.value = value;
           moveToHead(node);
       }
   }

   private void addNode(Node node) {
       node.prev = head;
       node.next = head.next;
       head.next.prev = node;
       head.next = node;
   }

   private void removeNode(Node node) {
       node.prev.next = node.next;
       node.next.prev = node.prev;
   }

   private void moveToHead(Node node) {
       removeNode(node);
       addNode(node);
   }

   private Node popTail() {
       Node res = tail.prev;
       removeNode(res);
       return res;
   }
}

使用示例:

public class Main {
   public static void main(String[] args) {
       LRUCache lRUCache = new LRUCache(2);
       lRUCache.put(1, 1); // 缓存是 {1=1}
       lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
       System.out.println(lRUCache.get(1));    // 返回 1
       lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
       System.out.println(lRUCache.get(2));    // 返回 -1 (未找到)
       lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
       System.out.println(lRUCache.get(1));    // 返回 -1 (未找到)
       System.out.println(lRUCache.get(3));    // 返回 3
       System.out.println(lRUCache.get(4));    // 返回 4
   }
}

输出结果应该与题目中的示例输出一致。这个实现使用了哈希表来快速查找节点,同时使用双向链表来维护节点的顺序和实现快速插入和删除操作。这样,我们就可以在 O(1) 的时间复杂度内完成 get 和 put 操作。

实现过程和步骤如下:

  1. 定义一个 Node 类,用于双向链表的节点,包含 key、value 以及前驱和后继节点的引用。
  2. 在 LRUCache 类中,定义一个 capacity 表示缓存的最大容量,一个 count 表示当前缓存中的节点数量,一个 cache 哈希表用于存储键和节点的映射,以及 head 和 tail 虚拟节点用于简化双向链表的操作。
  3. 实现 get 方法,首先在哈希表中查找键对应的节点,如果节点不存在,返回 -1。如果节点存在,则将该节点移动到链表的头部,表示最近被使用,然后返回节点的值。
  4. 实现 put 方法,如果键在哈希表中不存在,创建一个新的节点,并将其添加到链表的头部,同时添加到哈希表中。如果插入后节点数量超过了容量,则需要移除链表尾部的节点,并从哈希表中删除对应的条目。如果键已经存在,则更新对应的节点值,并将其移动到链表头部。
  5. addNode 方法用于将节点添加到链表头部,removeNode 方法用于从链表中移除节点,moveToHead 方法用于将节点移动到链表头部,popTail 方法用于移除链表尾部的节点。
  6. 在 Main 类的 main 方法中,我们创建了一个 LRUCache 实例,并执行了一系列的 put 和 get 操作来演示其功能。

V哥认为这个实现确保了 get 和 put 操作的平均时间复杂度为O(1)。get 操作直接通过哈希表查找节点,而 put 操作中,无论是更新现有节点还是添加新节点,都涉及到对双向链表的操作,这些操作的时间复杂度都是 O(1)。当缓存达到容量上限时,put 操作会移除最久未使用的节点,这也是 O(1) 时间复杂度的操作。

3. 小结一下

V哥的这个实现的关键在于维护一个双向链表,它可以帮助我们快速地访问、更新和删除最近最少使用的节点,同时使用哈希表来提供快速的查找能力。这样,我们就可以在 O(1) 的时间复杂度内完成所有的缓存操作。哈哈干净利索,回答完毕。

相关文章
|
2月前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
102 16
|
4月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
4月前
|
SQL 缓存 关系型数据库
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴因未能系统梳理MySQL缓存机制而在美团面试中失利。为此,尼恩对MySQL的缓存机制进行了系统化梳理,包括一级缓存(InnoDB缓存)和二级缓存(查询缓存)。同时,他还将这些知识点整理进《尼恩Java面试宝典PDF》V175版本,帮助大家提升技术水平,顺利通过面试。更多技术资料请关注公号【技术自由圈】。
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
|
5月前
|
缓存 监控 算法
小米面试题:多级缓存一致性问题怎么解决
【10月更文挑战第23天】在现代分布式系统中,多级缓存架构因其能够显著提高系统性能和响应速度而被广泛应用。
192 3
|
5月前
|
机器学习/深度学习 算法 Java
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
|
5月前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩在读者群中分享了关于分库分表的基因算法设计,旨在帮助大家应对一线互联网企业的面试题。文章详细介绍了分库分表的背景、分片键的设计目标和建议,以及基因法的具体应用和优缺点。通过系统化的梳理,帮助读者提升架构、设计和开发水平,顺利通过面试。
美团面试:百亿级分片,如何设计基因算法?
|
5月前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩分享分库分表的基因算法设计,涵盖分片键选择、水平拆分策略及基因法优化查询效率等内容,助力面试者应对大厂技术面试,提高架构设计能力。
美团面试:百亿级分片,如何设计基因算法?
|
存储 缓存 算法
昨天面试被问到的 缓存淘汰算法FIFO、LRU、LFU及Java实现
在高并发、高性能的质量要求不断提高时,我们首先会想到的就是利用缓存予以应对。 第一次请求时把计算好的结果存放在缓存中,下次遇到同样的请求时,把之前保存在缓存中的数据直接拿来使用。 但是,缓存的空间一般都是有限,不可能把所有的结果全部保存下来。那么,当缓存空间全部被占满再有新的数据需要被保存,就要决定删除原来的哪些数据。如何做这样决定需要使用缓存淘汰算法。 常用的缓存淘汰算法有:FIFO、LRU、LFU,下面我们就逐一介绍一下。
332 0
|
缓存
leetcode 146 LRU缓存
leetcode 146 LRU缓存
127 0
leetcode 146 LRU缓存
|
9月前
|
缓存 算法 索引
LeetCode146:LRU缓存
LeetCode146:LRU缓存
66 1

热门文章

最新文章