LRU LFU FIFO缓存以及代码实现

目录

一.LRU缓存

1.概念

2.实现原理

3.代码示例

二.LFU缓存

1.概念

2.实现原理

3.代码示例

4.优化代码

三.FIFO缓存

1.概念

 2.实现原理

3.代码示例

LinkedHashMap 构造函数

内部类和方法重写

工作机制

总结常见缓存策略的实现


一.LRU缓存

1.概念

LRU(Least Recently Used)缓存是一种缓存淘汰策略,用来在内存有限的情况下有效管理缓存内容。LRU策略会移除最近最少使用的缓存项,从而保留较常访问的数据。它通常使用哈希表和双向链表的组合来实现,以提供O(1)的时间复杂度用于插入和访问。

2.实现原理

  1. 数据结构:

    • 哈希表:快速查找缓存项,提供O(1)的时间复杂度。
    • 双向链表维护缓存项的访问顺序,使得尾部的节点代表最近最少使用的数据
  2. 操作:

    • 访问:如果访问一个缓存数据,将其移动到链表的头部。
    • 插入:新数据插入链表头部。
    • 淘汰:当缓存达到容量限制时,移除链表尾部的节点。

3.代码示例

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

class LRUCache<K,V> {
    class Node<K,V>{
        K key;
        V value;
        Node<K,V> prev;
        Node<K,V> next;
        public Node() {}
        public Node(K key,V value) {
            this.key=key;
            this.value=value;
        }
    }
    private Map<K,Node<K,V>> hash=new HashMap<>();
    int capacity;
    private Node<K,V> head,tail;
    public LRUCache(int capacity) {
        this.capacity=capacity;
        head=new Node();
        tail=new Node();
        head.next=tail;
        tail.prev=head;
    }
    public V get(K key) {
        Node<K,V> node=hash.get(key);
        if(node==null) {
            return null;
        }
        moveToHead(node);
        return node.value;
    }
    public void put(K key,V value) {
        Node<K,V> node=hash.get(key);
        if(node==null) {
            Node<K,V> newNode=new Node<K,V>(key,value);
            hash.put(key,newNode);
            addToHead(newNode);
            if(hash.size()>capacity) {
                //此时进行删除
                Node<K,V> temp=removeTail();
                hash.remove(temp.key);
            }
        } else {
            node.value=value;
            moveToHead(node);
        }
    }
    public Node<K,V> removeTail() {
        Node<K,V> ans=tail.prev;
        removeNode(ans);
        return ans;
    }
    public void removeNode(Node<K,V> node) {
        node.prev.next=node.next;
        node.next.prev=node.prev;
    }
    public void addToHead(Node<K,V> node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    public void moveToHead(Node<K,V> node) {
        removeNode(node);
        addToHead(node);
    }
}

:流程就是当我们插入到一个数据的时候将其放到双向链表头部位置,这里的head和tail是用来指向双向链表的头结点和尾结点的,俗称为哨兵位。对于已经存在节点,我们无论是查询还是修改值,都会影响这个节点使用的时间,所以每次在使用完之后将其移动到头节点的位置,对于移动节点的时候,这里做的是将其节点删除,在进行插入。所以每次删除节点的时候,tail所指向的节点就是我们要删除的节点。

二.LFU缓存

1.概念

LFU (Least Frequently Used) 缓存是一种缓存淘汰策略,用于在有限的内存中有效管理缓存内容。LFU缓存会移除使用频率最低的缓存项当缓存容量达到限制时,频率最低的项将被移除

2.实现原理

  1. 数据结构:

    • 哈希表:用于存储键到节点的映射,提供O(1)时间复杂度的查找。
    • 优先队列用来根据使用频率排序节点,频率最低的节点在队列顶部,提供O(log N)的插入和删除操作。
  2. 操作:

    • 访问:每次访问时增加项的频率,并重新调整优先队列的顺序。
    • 插入:如果新数据插入并且达到容量上限,将移除优先队列中最低频率的节点。

3.代码示例

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

public class LFUCache<K,V> {
    class Node<K,V>{
        K key;
        V value;
        int  frequency;
        public Node() {}
        public Node(K key,V value) {
            this.key=key;
            this.value=value;
            this.frequency=1;
        }
    }
    private int capacity;
    private Map<K,Node<K,V>> hash=new HashMap<>();
    private PriorityQueue<Node<K,V>> queue;
    public LFUCache(int capacity) {
        this.capacity=capacity;
        queue=new PriorityQueue<>((node1,node2)->node1.frequency-node2.frequency);
    }
    public void put(K key,V value) {
        if(capacity==0) {
            return;
        }
       Node<K,V> node=hash.get(key);
       if(node==null) {
           Node<K,V> newNode=new Node<>(key,value);
           hash.put(key,newNode);
           if(hash.size()>capacity) {
               Node<K,V> temp = queue.poll();
               hash.remove(temp.key);
           }
           queue.offer(newNode);
       } else {
           node.value=value;
           node.frequency++;
           queue.remove(node);
           queue.offer(node);
       }
    }
    public V get(K key) {
        Node<K,V> node=hash.get(key);
        if(node==null) {
            return null;
        }
        node.frequency++;
        queue.remove(node);
        queue.offer(node);
        return node.value;
    }
}

 注:这里的频率表示有点简单。在插入节点的时候,先将节点插入到hash中,然后再进行判断缓存大小是否大于容量,大于容量再将堆顶的元素删除。再元素插入到队列中。这时候发现一个问题,我们初始插入的节点频率为1,如果队列中频率都新节点大,那不是要删除新节点吗?

但是我们一想,如果新插入的节点就直接删除的话,那插入还有什么意思。

测试代码

 public static void main(String[] args) {
        LFUCache cache = new LFUCache(2);
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(1)); // 输出 1
        cache.put(3, 3); // 淘汰频率最低的键 2
        System.out.println(cache.get(2)); // 输出 null
        System.out.println(cache.get(3)); // 输出 3
        cache.put(4, 4); // 淘汰频率最低的键 1
        System.out.println(cache.get(1)); // 输出 null
        System.out.println(cache.get(3)); // 输出 3
        System.out.println(cache.get(4)); // 输出 null
    }

 

在调试的时候断点直接打在cache.put(4,4)这一行,队列里的元素频率都比4这个节点频率大。 但是我们肯定不想将4直接删除,所以要进行优化缓存策略。

4.优化代码


    import java.util.*;
    public class LFUCacheOptimized {
        class Node {
            int key, value, frequency;
            Node prev, next;

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

        class FrequencyList {
            Node head, tail;

            public FrequencyList() {
                head = new Node(-1, -1);
                tail = new Node(-1, -1);
                head.next = tail;
                tail.prev = head;
            }

            public void insert(Node node) {
                node.next = head.next;
                node.prev = head;
                head.next.prev = node;
                head.next = node;
            }

            public void remove(Node node) {
                node.prev.next = node.next;
                node.next.prev = node.prev;
            }

            public Node removeLast() {
                if (tail.prev == head) return null;
                Node node = tail.prev;
                remove(node);
                return node;
            }
        }

        private final int capacity;
        private Map<Integer, Node> cache;
        private Map<Integer, FrequencyList> frequencyMap;
        private int minFrequency;

        public LFUCacheOptimized(int capacity) {
            this.capacity = capacity;
            cache = new HashMap<>();
            frequencyMap = new HashMap<>();
            this.minFrequency = 0;
        }

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

        public void put(int key, int value) {
            if (capacity == 0) return;
            Node node = cache.get(key);
            if (node == null) {
                if (cache.size() == capacity) {
                    FrequencyList list = frequencyMap.get(minFrequency);
                    Node removedNode = list.removeLast();
                    cache.remove(removedNode.key);
                }
                node = new Node(key, value);
                cache.put(key, node);
                minFrequency = 1;
                frequencyMap.computeIfAbsent(1, k -> new FrequencyList()).insert(node);
            } else {
                node.value = value;
                updateFrequency(node);
            }
        }

        private void updateFrequency(Node node) {
            int freq = node.frequency;
            FrequencyList list = frequencyMap.get(freq);
            list.remove(node);
            if (freq == minFrequency && list.head.next == list.tail) {
                minFrequency++;
            }
            node.frequency++;
            frequencyMap.computeIfAbsent(node.frequency, k -> new FrequencyList()).insert(node);
        }

        public static void main(String[] args) {
            LFUCacheOptimized cache = new LFUCacheOptimized(2);
            cache.put(1, 1);
            cache.put(2, 2);
            System.out.println(cache.get(1)); // 输出 1
            cache.put(3, 3); // 淘汰频率最低的键 2
            System.out.println(cache.get(2)); // 输出 -1
            System.out.println(cache.get(3)); // 输出 3
            cache.put(4, 4); // 淘汰频率最低的键 1
            System.out.println(cache.get(1)); // 输出 -1
            System.out.println(cache.get(3)); // 输出 3
            System.out.println(cache.get(4)); // 输出 4
        }
    }
    

 解析:

 frequencyMap.computeIfAbsent(node.frequency, k -> new FrequencyList()).insert(node);

当使用frequencyMap.computeIfAbsent(node.frequency, k -> new FrequencyList())时,如果node.frequency已经存在于frequencyMap中,computeIfAbsent方法将直接返回与node.frequency关联的FrequencyList实例,而不是执行提供的映射函数k -> new FrequencyList()

在这种情况下,频率存在,即Map不会为该频率创建新的FrequencyList对象,直接在现有的FrequencyList中插入节点。

 注:如果涉及多线程的场景下,使用ConcurrentHashMap更为妥当。

三.FIFO缓存

1.概念

FIFO(First-In-First-Out)缓存是一种简单但有效的缓存淘汰策略,当缓存达到容量时淘汰最早插入的缓存项。

 2.实现原理

  1. 数据结构:
    • LinkedHashMap:保持插入顺序并在达到容量上限时自动删除最早的缓存项。
  2. 操作:
    • 插入: 新数据插入时按顺序放置。
    • 淘汰: 当容量达到限制时,自动移除链表中的最早元素。

3.代码示例


    import java.util.LinkedHashMap;
    import java.util.Map;

     class FIFOCache<K, V> {
        private final int capacity;
        private final LinkedHashMap<K, V> cache;

        public FIFOCache(int capacity) {
            this.capacity = capacity;
            // LinkedHashMap构造方法的第三个参数为false,表示根据插入顺序排序
            this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, false) {
                @Override
                protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                    // 当缓存大小超过设置的容量时,移除最早插入的元素
                    return size() > FIFOCache.this.capacity;
                }
            };
        }

        // 从缓存获取数据
        public V get(K key) {
            return cache.get(key);
        }

        // 向缓存插入数据
        public void put(K key, V value) {
            cache.put(key, value);
        }

        // 测试FIFO缓存功能
        public static void main(String[] args) {
            FIFOCache<String, Integer> fifoCache = new FIFOCache<>(2);
            fifoCache.put("A", 1);
            fifoCache.put("B", 2);
            System.out.println(fifoCache.get("A")); // 输出 1

            fifoCache.put("C", 3); // 淘汰最早插入的键 A

            System.out.println(fifoCache.get("A")); // 输出 null,A 已被淘汰
            System.out.println(fifoCache.get("B")); // 输出 2,B 仍然存在

            fifoCache.put("D", 4); // 淘汰最早插入的键 B

            System.out.println(fifoCache.get("B")); // 输出 null,B 已被淘汰
            System.out.println(fifoCache.get("C")); // 输出 3,C 仍然存在
            System.out.println(fifoCache.get("D")); // 输出 4,D 已插入
        }
    }

LinkedHashMap 构造函数

复制

new LinkedHashMap<K, V>(capacity, 0.75f, false)
  • 参数解析
    • capacity:表示构造 LinkedHashMap 的初始容量,实际容量可能会根据负载因子和存入的元素动态增大。
    • 0.75f:这是装载因子(load factor),用于控制LinkedHashMap何时进行扩容。在目前的应用中,不会直接影响缓存策略。
    • false:指示LinkedHashMap根据插入顺序存储条目,而不是访问顺序。传入false确保访问顺序不更改原始的插入顺序,从而实现FIFO逻辑。

内部类和方法重写

复制

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() > FIFOCache.this.capacity;
}
  • 重写方法 removeEldestEntry
    • LinkedHashMap中,removeEldestEntry是被用于判断在每次插入新条目时,是否需要移除最老的条目的方法。
    • 条件size() > FIFOCache.this.capacity,检查当前缓存大小是否超过指定容量。
    • 如果缓存大小超过设定容量,将移除最早插入的元素(最终以FIFO顺序删除)。

工作机制

  • 插入条目:每当一个新条目插入LinkedHashMap时,系统将调用removeEldestEntry方法来决定是否移除旧条目。
  • FIFO策略实现:因为LinkedHashMap被配置为按插入顺序存储,并在超出容量时移除最老的条目,因此实现了FIFO策略。

总结常见缓存策略的实现

  1. LRU(Least Recently Used)缓存

    • 使用双向链表和哈希表实现。
    • 淘汰最近最少使用的数据。
  2. LFU(Least Frequently Used)缓存

    • 使用优先队列和哈希表实现。
    • 淘汰使用频率最低的数据。
  3. FIFO(First-In-First-Out)缓存

    • 使用LinkedHashMap的特殊配置实现。
    • 淘汰最早插入的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值