首页 > 编程语言 >高级java每日一道面试题-2024年9月30日-算法篇-LRU是什么?如何实现?

高级java每日一道面试题-2024年9月30日-算法篇-LRU是什么?如何实现?

时间:2024-09-30 21:23:19浏览次数:9  
标签:node Node 面试题 缓存 java 30 链表 算法 LRU

如果有遗漏,评论区告诉我进行补充

面试官: LRU是什么?如何实现?

我回答:

LRU(Least Recently Used)是一种常用的缓存淘汰策略,用于在缓存满时决定哪些数据应该被移除。LRU算法的基本思想是:当缓存达到其容量上限时,最近最少使用的数据会被优先淘汰。这种策略假设最近使用的数据在未来也会被频繁访问。

LRU算法概述

LRU算法是一种缓存淘汰策略,其核心思想是:如果一个数据在最近一段时间没有被访问到,那么在未来被访问的可能性也很小。因此,当缓存空间已满时,LRU算法会选择最近最少使用的数据进行淘汰。

LRU算法广泛应用于操作系统中的页面置换、数据库查询优化、Web缓存等场景,是最大化缓存命中率的有效手段之一。

LRU算法的实现原理

LRU的实现

LRU的实现通常需要一个数据结构来同时支持快速查找和插入/删除操作。常用的数据结构是哈希表(HashMap)和双向链表(Doubly Linked List)的结合体。

数据结构
  • 哈希表:用于快速查找缓存中的元素。
  • 双向链表:用于维护元素的访问顺序,最近访问的元素放在链表头部,最久未访问的元素放在链表尾部。
基本操作
  1. 插入

    • 如果新插入的键已经在缓存中,则更新其值,并将其移动到链表头部。
    • 如果新插入的键不在缓存中,且缓存已满,则移除链表尾部的元素,并将新元素插入到链表头部。
  2. 访问

    • 如果访问的键在缓存中,则将其移动到链表头部。
    • 如果访问的键不在缓存中,则返回null或其他默认值。
  3. 删除

    • 如果删除的键在缓存中,则从链表和哈希表中移除该元素。
    • 如果删除的键不在缓存中,则不进行任何操作。

LRU算法的实现需要满足以下几个要求:

  1. 查找快:能够迅速找到缓存中的数据。
  2. 插入快:能够快速地将新数据插入到缓存中。
  3. 删除快:能够高效地删除缓存中的数据。
  4. 维护顺序:需要维护数据的访问顺序,以便在缓存空间不足时淘汰最近最少使用的数据。

代码实现

下面是一个使用Java实现LRU缓存的示例:

import java.util.HashMap;
import java.util.Map;

public class LRUCache<K, V> {
    private final int capacity;
    private final Map<K, Node<K, V>> map;
    private final DoublyLinkedList<K, V> list;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
        this.list = new DoublyLinkedList<>();
    }

    public V get(K key) {
        if (map.containsKey(key)) {
            Node<K, V> node = map.get(key);
            list.moveToHead(node); // 将访问的节点移到链表头部
            return node.value;
        }
        return null;
    }

    public void put(K key, V value) {
        if (map.containsKey(key)) {
            Node<K, V> node = map.get(key);
            node.value = value; // 更新节点的值
            list.moveToHead(node); // 将更新的节点移到链表头部
        } else {
            if (map.size() >= capacity) {
                Node<K, V> removedNode = list.removeTail(); // 移除链表尾部的节点
                map.remove(removedNode.key); // 从哈希表中移除对应的键
            }
            Node<K, V> newNode = new Node<>(key, value);
            list.addHead(newNode); // 将新节点添加到链表头部
            map.put(key, newNode); // 在哈希表中添加新的键值对
        }
    }

    private static class Node<K, V> {
        K key;
        V value;
        Node<K, V> prev;
        Node<K, V> next;

        Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    private static class DoublyLinkedList<K, V> {
        private Node<K, V> head;
        private Node<K, V> tail;

        public void addHead(Node<K, V> node) {
            if (head == null) {
                head = tail = node;
            } else {
                node.next = head;
                head.prev = node;
                head = node;
            }
        }

        public void moveToHead(Node<K, V> node) {
            if (node == head) return; // 如果节点已经是头结点,则无需移动
            removeNode(node);
            addHead(node);
        }

        public Node<K, V> removeTail() {
            if (tail == null) return null;
            Node<K, V> node = tail;
            removeNode(tail);
            return node;
        }

        private void removeNode(Node<K, V> node) {
            if (node.prev != null) {
                node.prev.next = node.next;
            } else {
                head = node.next;
            }
            if (node.next != null) {
                node.next.prev = node.prev;
            } else {
                tail = node.prev;
            }
            node.prev = null;
            node.next = null;
        }
    }

    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(2);
        cache.put(1, "one");
        cache.put(2, "two");
        System.out.println(cache.get(1)); // 输出: one
        cache.put(3, "three"); // 移除最近最少使用的 2
        System.out.println(cache.get(2)); // 输出: null
        cache.put(4, "four"); // 移除最近最少使用的 1
        System.out.println(cache.get(1)); // 输出: null
        System.out.println(cache.get(3)); // 输出: three
        System.out.println(cache.get(4)); // 输出: four
    }
}

解释

  1. LRUCache 类

    • capacity:缓存的最大容量。
    • map:哈希表,用于存储键和对应的节点。
    • list:双向链表,用于维护节点的访问顺序。
  2. get 方法

    • 如果键存在于缓存中,将对应的节点移动到链表头部,并返回其值。
    • 如果键不存在于缓存中,返回null。
  3. put 方法

    • 如果键已经存在于缓存中,更新其值并将节点移动到链表头部。
    • 如果键不存在于缓存中且缓存已满,移除链表尾部的节点,并将新节点添加到链表头部。
    • 如果键不存在于缓存中且缓存未满,直接将新节点添加到链表头部。
  4. Node 类

    • 表示双向链表中的一个节点,包含键、值以及前驱和后继指针。
  5. DoublyLinkedList 类

    • 实现了双向链表的基本操作,包括添加节点到头部、移动节点到头部、移除节点等。

LRU算法的性能分析

LRU算法的性能主要取决于哈希表和双向链表的操作效率。由于哈希表的查找、插入和删除操作的时间复杂度都是O(1),双向链表的插入、删除和移动操作的时间复杂度也都是O(1)(在已知节点位置的情况下),因此LRU算法的整体时间复杂度可以认为是O(1)。

然而,需要注意的是,在实际应用中,由于哈希表的冲突和链表节点的移动等操作,LRU算法的实际性能可能会受到一定影响。此外,当缓存数据量很大时,哈希表和链表的内存开销也需要考虑。

LRU算法的改进和优化

针对LRU算法的不足,有一些改进和优化方法:

  1. LRU-K算法:将“最近使用过1次”的判断标准扩展为“最近使用过K次”,以减少缓存污染问题。LRU-K算法需要多维护一个队列来记录所有缓存数据被访问的历史。
  2. Two Queues(2Q)算法:使用两个缓存队列,一个是FIFO队列,一个是LRU队列。新数据先放入FIFO队列,当数据再次被访问时,将其移到LRU队列。这种算法结合了FIFO和LRU的优点。
  3. MQ算法:根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级。新数据放入最低优先级的队列,当数据的访问次数达到一定次数时,将其提升到更高优先级的队列。

总结

综上所述,LRU算法是一种高效且广泛应用的缓存淘汰策略。在Java中,可以通过使用哈希表和双向链表的组合来实现LRU缓存。同时,也需要根据实际应用场景和需求对LRU算法进行改进和优化。

标签:node,Node,面试题,缓存,java,30,链表,算法,LRU
From: https://blog.csdn.net/qq_43071699/article/details/142663710

相关文章

  • 高级java每日一道面试题-2024年9月30日-服务器篇[Redis篇]-Redis持久化有几种方式?
    如果有遗漏,评论区告诉我进行补充面试官:Redis持久化有几种方式?我回答:Redis是一个高性能的键值存储系统,常用于缓存、消息队列和实时数据分析等场景。为了保证数据的持久性,Redis提供了两种主要的持久化方式:RDB(RedisDatabaseBackup)和AOF(AppendOnlyFile)。这两种方......
  • JavaSE的小结10
    第1章-第10节一、知识点网络编程。二、目标理解前后端交互过程。掌握网络编程的基本概念。三、内容分析重点网络编程基本概念。前后端交互过程。难点前后端交互过程。四、内容1、网络编程网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过......
  • 校测 2024 0930 数学
    0-30-0,数学还只打了暴力,菜就多练Problem1.facsum省流:\(f(n)=(\sum\limits_{d\midn}\varphi(d))^m(\sum\limits_{d\midn}\sigma_0(d)\mu(\frac{n}{d})\frac{n}{d})\)求\(\sum\limits_{i=1}^nf(i)\bmod1e9+7\)大概是把前面的区域以后再来探索吧Problem2.groupM......
  • Java学习第七天--面向对象
    目录1.学什么 2.类2.1类的组成2.2类与对象的关系3.对象内存图 4.成员变量和局部变量5.this关键字6.构造方法6.1构造器6.2格式:6.3执行时机:6.4构造方法作用6.5构造方法注意事项6.5.1构造方法的创建6.5.2构造方法的重载6.5.3推荐的使用方式7.封装7.1合理隐藏,......
  • 20240930
    TheOnlyWaytotheDestination首先假如两个墙之间的间隔大于等于二了,那么就直接输出\(no\),如果能在图的空隙中找到一个\(2*2\)的矩形,那么也是输出\(no\),然后我们可以把每一列看成一个点,再把每个空隙看成一条边即可,用并查集维护ASimpleMSTProblem一个性质我......
  • 9.30
    今天学习java生成二维码importcom.google.zxing.WriterException;importcom.google.zxing.common.BitMatrix;importcom.google.zxing.qrcode.QRCodeWriter;importcom.google.zxing.client.j2se.MatrixToImageWriter;importjava.awt.image.BufferedImage;importjava.i......
  • java计算机毕业设计社区食堂就餐管理系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着城市化进程的加速和人口老龄化的加剧,社区作为居民生活的基本单元,其服务功能的重要性日益凸显。社区食堂作为连接居民日常生活的重要一环,不仅承载......
  • Java 中的5个代码性能提升技巧,最高提升近10倍
    Java中的5个代码性能提升技巧,最高提升近10倍 文章持续更新,可以关注公众号程序猿阿朗或访问未读代码博客。本文 Github.com/niumoo/JavaNotes 已经收录,欢迎Star。这篇文章介绍几个Java开发中可以进行性能优化的小技巧,虽然大多数情况下极致优化代码是没有必要的,但是作......
  • 什么是javascript的事件循环
    JavaScript的事件循环(EventLoop)是其执行机制的核心,用来处理异步操作,使得JavaScript能够实现非阻塞式的单线程异步编程。为了理解事件循环,首先要了解JavaScript是单线程的语言,这意味着它一次只能执行一个任务。但在实际应用中,比如I/O操作(网络请求、定时器、用户事......
  • Java语言之数据类型与变量
    Java的数据类型主要分为两类基本数据类型:整形(包括:字节型:byte、1个字节,短整型:short、两个字节,整形:int、4个字节,长整型:long、8个字节),字符型:char、一个字节,浮点型(包括:单精度浮点型float、4个字节,双精度浮点型:double、8个字节),布尔类型:boolean,java并没有规定几个字节。java中没有......