高级java每日一道面试题-2024年9月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缓存的示例:

java 复制代码
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算法进行改进和优化。

相关推荐
猿究院_夏燊1 小时前
Spring Bean 生命周期
java·后端·spring
Wang's Blog2 小时前
Java 项目实战: 交友项目:登录链路回顾与今日任务地图
java·开发语言·状态模式·交友
信誓旦旦的程序猿2 小时前
【Python量化实战宝典 #08】可转债与债券接口手册:转股价·溢价率·纯债价值(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
砚底藏山河2 小时前
python量化:行情数据回溯
java·开发语言·python·金融·maven
Jlzn88882 小时前
华东电芯分选通道数怎么配才不堵线
java·人工智能·架构·自动化·制造
香吧香3 小时前
GRPC连接机制---长连接分析
java·网络与传输协议
我是人✓3 小时前
SpringBoot项目的五种创建方式
java·spring boot·后端
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 阿里云 OSS 入门案例:从创建 Bucket 到上传拿到可访问 URL
java·阿里云·交友
我不会起名字3224 小时前
Redis 突然只能读不能写:MISCONF 报错的 3 类触发条件与 4 步恢复
运维·redis·持久化·排查·misconf
裕晟资质规划4 小时前
三类涉密资质延续时限差异分析:军工60日与集成印制3个月的边界
java·服务器·前端·网络·安全