LRU和LFU简介与代码演练

文章介绍了LRU(最近最少使用)和LFU(最不经常使用)两种缓存替换算法,分别阐述了它们的基本思想和实现逻辑。LRU基于时间局部性,使用LinkedHashMap数据结构,当缓存满时删除最久未使用的数据。LFU基于频率局部性,维护访问频率的链表,淘汰访问频率最低的数据。文章还提供了Java实现的代码示例。
摘要由CSDN通过智能技术生成

简介

LRU(Least Recently Used)和LFU(Least Frequently Used)是两种常见的缓存替换算法。

LRU是基于最近使用时间的缓存替换算法。它的基本思想是,当缓存空间不足时,优先淘汰最长时间未被访问的数据。LRU算法维护一个访问顺序链表(或双向链表),每次访问一个数据时,将其移动到链表的头部。当需要淘汰数据时,选择链表尾部的数据进行删除。

LFU则是基于访问频率的缓存替换算法。它的核心思想是,当缓存空间不足时,优先淘汰访问频率最低的数据。LFU算法维护一个频率计数器,用于记录每个数据被访问的次数。每次访问一个数据时,将对应数据的访问次数加1。当需要淘汰数据时,选择访问次数最低的数据进行删除。

LRU和LFU都是为了在有限的缓存空间中提供尽可能高的缓存命中率。LRU更注重保留最近被访问的数据,而LFU更注重保留访问频率较高的数据。

在实际应用中,可以根据具体的场景选择合适的缓存替换算法。LRU算法适用于访问模式具有时间局部性的场景,而LFU算法适用于访问模式具有频率局部性的场景。有些系统还会结合两种算法,根据实际情况动态选择使用哪种算法。

当我们看这个代码的时候,首先需要理解两个缓存算法的概念:LRU(最近最少使用)和LFU(最不经常使用)。


实现逻辑

  • LRU缓存算法逻辑:

LRU缓存算法基本思想是保留最近被访问过的数据项,当容量超过预设阈值时,将最久未被访问的数据项从缓存中移除。

在代码中,我们使用LinkedHashMap作为LRU缓存的数据结构。LinkedHashMap是一个有序的HashMap,通过重写removeEldestEntry方法来控制是否移除最老的数据项。具体逻辑如下:

在构造函数中,我们初始化一个LinkedHashMap作为缓存,并重写了removeEldestEntry方法,使其在缓存大小超过capacity时返回true,表示需要移除最老的数据项。

get方法用于从缓存中获取数据。如果数据项存在于缓存中,则将其移动到链表的尾部(表示最近访问过),然后返回数据项的值;否则,返回null。

put方法用于向缓存中插入数据。如果数据项已存在,则更新数据值,并将其移动到链表尾部。如果数据项不存在,我们检查缓存是否已满,如果满了,则移除链表头部的数据项(最久未被访问的数据项),然后插入新数据项到链表尾部。

  • LFU缓存算法逻辑:

LFU缓存算法基本思想是保留访问频率最高的数据项,当容量超过预设阈值时,将访问频率最低的数据项从缓存中移除。

在代码中,我们使用一个自定义的LFUCache类来实现LFU缓存。具体逻辑如下:

在构造函数中,我们初始化一个HashMap作为缓存,并设定缓存容量。

get方法用于从缓存中获取数据。如果数据项存在于缓存中,则将其访问频率增加,并通过updateNode方法将其在链表中按照访问频率调整位置,然后返回数据项的值;否则,返回null。

put方法用于向缓存中插入数据。如果数据项已存在,则更新数据值,并将其访问频率增加,并通过updateNode方法将其在链表中按照访问频率调整位置。如果数据项不存在,我们检查缓存是否已满,如果满了,则移除链表头部的数据项(访问频率最低的数据项),然后插入新数据项到链表尾部,并设定访问频率为1。

updateNode 这段代码是用于在LFU缓存算法中更新节点位置的逻辑。它会在以下情况下执行:

  1. 当要更新的节点的下一个节点不为空,并且当前节点的频率大于或等于下一个节点的频率时,会进入循环while (curr.next != null && curr.frequency >= curr.next.frequency)。这意味着当前节点的频率与下一个节点的频率相同或更高。
  2. 如果在循环内,发现当前节点curr不等于要更新的节点node,则表示需要将节点node移动到当前节点curr的位置。

在LFU缓存算法中,当访问频率增加时,我们需要将节点移动到适当的位置,以保持链表中节点按照访问频率从低到高的顺序排列。这段代码就是负责处理这个移动的逻辑。

例如,假设链表中有以下节点(按照频率从低到高的顺序):A -> B -> C -> D。

  • 当访问节点B时,B的频率增加,变为了2。此时,循环将curr从B移动到C。
  • 如果访问节点C,C的频率增加,变为了3。由于C的频率大于D(频率为2),所以循环将curr从C移动到D的位置,链表变为A -> B -> D -> C。

在LFUCache类中,updateNode方法会在get和put操作中调用,用于调整节点的位置。通过这个逻辑,我们可以确保链表中节点按照频率从低到高的顺序排列。


package com.javayh.mv.common.util;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;

/**
 * <p>
 *
 * </p>
 *
 * @author hai ji
 * @version 1.0.0
 * @since 2023-07-05
 */
class LRUCache<K, V> {
    private int capacity;
    private LinkedHashMap<K, V> cache;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > capacity;
            }
        };
    }

    public V get(K key) {
        return cache.getOrDefault(key, null);
    }

    public void put(K key, V value) {
        cache.put(key, value);
    }
}

class LFUCache<K, V> {
    private int capacity;
    private Map<K, Node<K, V>> cache;
    private Node<K, V> head;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>(capacity);
    }

    public V get(K key) {
        Node<K, V> node = cache.get(key);
        if (node != null) {
            node.frequency++;
            updateNode(node);
            return node.value;
        }
        return null;
    }

    public void put(K key, V value) {
        if (capacity == 0) {
            return;
        }

        Node<K, V> node = cache.get(key);
        if (node != null) {
            node.value = value;
            node.frequency++;
            updateNode(node);
        } else {
            if (cache.size() >= capacity) {
                K evictKey = head.keys.iterator().next();
                head.keys.remove(evictKey);
                cache.remove(evictKey);
            }

            Node<K, V> newNode = new Node<>(key, value);
            newNode.frequency = 1;
            cache.put(key, newNode);
            if (head == null || head.frequency > 1) {
                Node<K, V> newHead = new Node<>(null, null);
                newHead.next = head;
                if (head != null) {
                    head.prev = newHead;
                }
                head = newHead;
            }
            head.keys.add(key);
        }
    }

    /*
     * 这段代码是用于在LFU缓存算法中更新节点位置的逻辑。它会在以下情况下执行:
     *
     * 当要更新的节点的下一个节点不为空,并且当前节点的频率大于或等于下一个节点的频率时,会进入循环while (curr.next != null && curr.frequency >= curr.next.frequency)。这意味着当前节点的频率与下一个节点的频率相同或更高。
     *
     * 如果在循环内,发现当前节点curr不等于要更新的节点node,则表示需要将节点node移动到当前节点curr的位置。
     *
     * 在LFU缓存算法中,当访问频率增加时,我们需要将节点移动到适当的位置,以保持链表中节点按照访问频率从低到高的顺序排列。这段代码就是负责处理这个移动的逻辑。
     *
     * 例如,假设链表中有以下节点(按照频率从低到高的顺序):A -> B -> C -> D。
     *
     * 当访问节点B时,B的频率增加,变为了2。此时,循环将curr从B移动到C。
     *
     * 如果访问节点C,C的频率增加,变为了3。由于C的频率大于D(频率为2),所以循环将curr从C移动到D的位置,链表变为A -> B -> D -> C。
     *
     * 在LFUCache类中,updateNode方法会在get和put操作中调用,用于调整节点的位置。通过这个逻辑,我们可以确保链表中节点按照频率从低到高的顺序排列。
     */
    private void updateNode(Node<K, V> node) {
        Node<K, V> curr = node;
        while (curr.next != null && curr.frequency >= curr.next.frequency) {
            curr = curr.next;
        }

        if (curr != node) {
            curr.prev.next = node;
            node.prev = curr.prev;

            if (node.next != null) {
                node.next.prev = curr;
            }
            curr.next = node.next;
            node.next = curr;
            curr.prev = node;
        }

        LinkedHashSet<K> keys = node.keys;
        if (!keys.isEmpty()) {
            K key = keys.iterator().next();
            keys.remove(key);
            keys.add(key);
        }
    }

    private static class Node<K, V> {
        private K key;
        private V value;
        private int frequency;
        //节点的prev指向频率较低的节点,next指向频率较高的节点。当访问某个数据项时,我们通过更新节点的频率和调整节点在链表中的位置来实现LFU缓存算法的逻辑。
        private Node<K, V> prev;
        private Node<K, V> next;
        private LinkedHashSet<K> keys;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.frequency = 0;
            this.prev = null;
            this.next = null;
            this.keys = new LinkedHashSet<>();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // LRU Cache example  长时间为访问
        LRUCache<Integer, String> lruCache = new LRUCache<>(3);
        lruCache.put(1, "One");
        lruCache.put(2, "Two");
        lruCache.put(3, "Three");
        System.out.println(lruCache.get(2)); // Output: Two
        lruCache.put(4, "Four");
        System.out.println(lruCache.get(1)); // Output: null (evicted)
        System.out.println(lruCache.get(4)); // Output: Four

        // LFU Cache example 访问频次最低的
        LFUCache<Integer, String> lfuCache = new LFUCache<>(3);
        lfuCache.put(1, "One");
        lfuCache.put(2, "Two");
        lfuCache.put(3, "Three");
        System.out.println(lfuCache.get(2)); // Output: Two
        System.out.println(lfuCache.get(2)); // Output: Two
        lfuCache.put(4, "Four");
        System.out.println(lfuCache.get(2)); // Output: Two
        System.out.println(lfuCache.get(1)); // Output: null (evicted)
        System.out.println(lfuCache.get(4)); // Output: Four
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小杨同学~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值