详解LFU(最不经常使用)算法及Java实现

概述:
在上一篇中,详解了LRU(最近最少使用)算法,这一篇来讲述LFU(Least Frequently Used),即最不经常使用,也是一种页面置换算法。它的淘汰策略是选择使用频次最少的作为淘汰对象,满足一下几个约束:

  1. 使用get(K key)方法获取值时,其使用频次加一
  2. 使用put(K key, V value)方法添加时,如果key在缓存中已存在,则更新对应的value,并且其使用频次加一;否则判断缓存是否已满,是则移除缓存中使用频次最少的key,若有多个key的使用频次最少,则移除最旧的那个key;然后把新的key添加到缓存中,其对应的使用频次置为1。
    下面来介绍需要使用怎样的数据结构来满足上述约束。

实现:
对于每个key需要保存对应的value和使用频次frequency,为此可以将这个两个封装起来,定义一个类Pair,

private class Pair {
        V value;
        int frequency;

        public Pair(V value, int frequency) {
            this.value = value;
            this.frequency = frequency;
        }
    }

使用K-V键值对这种map结构可以使得get和put的时间复杂度为O(1),即定义

Map<K, Pair> keyMap;

通过keyMap可以快速的获取key对应的value和frequency;

怎么知道哪个key的frequency最小呢?把每个key拿出来依次比较?显然是不合理的,我们可以在全局范围内维护一个最小频次minFrequency;

上面约束的第二点说到,minFrequency可能对应多个key,这些key要怎么弄存储?我们需要再定义一个Map<K, V>用来存frequency对应的key集合,这里很显然K对应frequency,用Integer类就好,V用什么类型呢?当要删除minFrequency对应的key时,若key有多个则删除最旧的,也就是要满足先进先出原则,同事又要满足删除key的这个操作的时间复杂度为O(1),那就用队列+set这种数据结构把,即LinkedHashSet,最终Map定义为

Map<Integer, LinkedHashSet<K>> frequencyMap;

get方法的实现,

public V get(K key) {
        if (!keyMap.containsKey(key)) {
            return null;
        }
        // 每访问一次key,对应的频次加一
        increaseFrequency(key);
        return keyMap.get(key).value;
    }

put方法的实现,

public void put(K key, V value) {
        if (this.capacity < 1) {
            return;
        }
        if (keyMap.containsKey(key)) {
            keyMap.get(key).value = value;
            increaseFrequency(key);
            return;
        }
        // 如果容量满了,先剔除最近最少使用的key
        if (this.capacity == keyMap.size()) {
            removeMinFrequency();
        }
        // 更新
        this.minFrequency = 1;
        keyMap.put(key, new Pair(value, this.minFrequency));
        frequencyMap.putIfAbsent(this.minFrequency, new LinkedHashSet<>());
        frequencyMap.get(this.minFrequency).add(key);
    }

这两个方法逻辑都比较清晰,就不详细讲解了,下面主要来看看频次加一的方法increaseFrequency(K key),

private void increaseFrequency(K key) {
        // 频次加一之前,先获取key的频次
        int frequency = keyMap.get(key).frequency;
        // 从该frequency对应的key集合中剔除key
        LinkedHashSet<K> keys = frequencyMap.get(frequency);
        keys.remove(key);
        if (keys.isEmpty()) {
            frequencyMap.remove(frequency);
            // 若剔除的频次刚好等于全局的最小频次,则要更新minFrequency
            // 因为该minFrequency已没有对应的key了
            if (frequency == this.minFrequency) {
                this.minFrequency++;
            }
        }
        // 频次加一,更新keyMap与frequencyMap
        frequency += 1;
        keyMap.get(key).frequency = frequency;
        frequencyMap.putIfAbsent(frequency, new LinkedHashSet<>());
        frequencyMap.get(frequency).add(key);
    }

还有删除最不经常使用的key方法removeMinFrequency(),

private void removeMinFrequency() {
        // 根据全局的最小频次minFrequency获得对应的key集合
        LinkedHashSet<K> keys = frequencyMap.get(this.minFrequency);
        // key是按顺序添加的,因此需要移除最先添加的,即顺序遍历的第一个
        K key = keys.iterator().next();
        keys.remove(key);
        if (keys.isEmpty()) {
            frequencyMap.remove(this.minFrequency);
            // 这里因为调用本方法的前提是容量满了,
            // 而每新添加一个key,它对应的频次是1,minFrequency也会置为1
            // 故这里不需要更新minFrequency,在put方法中已更新
        }
        // 接着再从keyMap里移除key
        keyMap.remove(key);
    }

最后是完整代码,

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

public class LFUCache<K, V> {

    int capacity;
    int minFrequency;
    Map<K, Pair> keyMap;
    Map<Integer, LinkedHashSet<K>> frequencyMap;

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

    public V get(K key) {
        if (!keyMap.containsKey(key)) {
            return null;
        }
        // 每访问一次key,对应的频次加一
        increaseFrequency(key);
        return keyMap.get(key).value;
    }

    public void put(K key, V value) {
        if (this.capacity < 1) {
            return;
        }
        if (keyMap.containsKey(key)) {
            keyMap.get(key).value = value;
            increaseFrequency(key);
            return;
        }
        // 如果容量满了,先剔除最近最少使用的key
        if (this.capacity == keyMap.size()) {
            removeMinFrequency();
        }
        // 更新
        this.minFrequency = 1;
        keyMap.put(key, new Pair(value, this.minFrequency));
        frequencyMap.putIfAbsent(this.minFrequency, new LinkedHashSet<>());
        frequencyMap.get(this.minFrequency).add(key);
    }

    private void increaseFrequency(K key) {
        // 频次加一之前,先获取key的频次
        int frequency = keyMap.get(key).frequency;
        // 从该frequency对应的key集合中剔除key
        LinkedHashSet<K> keys = frequencyMap.get(frequency);
        keys.remove(key);
        if (keys.isEmpty()) {
            frequencyMap.remove(frequency);
            // 若剔除的频次刚好等于全局的最小频次,则要更新minFrequency
            // 因为该minFrequency已没有对应的key了
            if (frequency == this.minFrequency) {
                this.minFrequency++;
            }
        }
        // 频次加一,更新keyMap与frequencyMap
        frequency += 1;
        keyMap.get(key).frequency = frequency;
        frequencyMap.putIfAbsent(frequency, new LinkedHashSet<>());
        frequencyMap.get(frequency).add(key);
    }

    private void removeMinFrequency() {
        // 根据全局的最小频次minFrequency获得对应的key集合
        LinkedHashSet<K> keys = frequencyMap.get(this.minFrequency);
        // key是按顺序添加的,因此需要移除最先添加的,即顺序遍历的第一个
        K key = keys.iterator().next();
        keys.remove(key);
        if (keys.isEmpty()) {
            frequencyMap.remove(this.minFrequency);
            // 这里因为调用本方法的前提是容量满了,
            // 而每新添加一个key,它对应的频次是1,minFrequency也会置为1
            // 故这里不需要更新minFrequency,在put方法中已更新
        }
        // 接着再从keyMap里移除key
        keyMap.remove(key);
    }

    private class Pair {
        V value;
        int frequency;

        public Pair(V value, int frequency) {
            this.value = value;
            this.frequency = frequency;
        }
    }
}

至此,LFU算法的详解与实现到这里结束

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

b17a

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

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

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

打赏作者

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

抵扣说明:

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

余额充值