LFU算法Java实现

Redis内存淘汰策略中会使用到 LFU 算法,以下是简单说明及Java代码实现。

一、前言

LFU(The Least Frequently Used)即最不经常使用算法。其原理是如果一个数据最近被访问次数不多,那么将来它被使用的可能也更低,因此在内存达到一定阈值时,将最不经常使用的数据淘汰的一种策略算法。

二、图解

TODO 图解说明后期有空再补充

三、LFU 算法 Java 实现

3.1 定义节点类

节点Node类中应包含 key(键)、value(值)、cnt(使用频次)和prev(前置节点)、next(后置节点)指针

import lombok.Getter;
import lombok.Setter;

/**
 * 节点Node
 */
@Getter
@Setter
public class Node {

    /**
     * 节点键key
     */
    private int key;

    /**
     * 节点值value
     */
    private int value;

    /**
     * 使用频次
     */
    private int cnt;

    /**
     * 前置节点
     */
    private Node prev;

    /**
     * 后置节点
     */
    private Node next;

    public Node() {
    }

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

}

注意:代码中 @Getter、@Setter 使用的是 lombok 插件,如果没有安装直接删除这两个注解和对应 import 导入逻辑,再手工给所有属性添加 Getter/Setter 方法即可,后续代码也是不做另外说明;

3.2 双向链表实现

双向链表中定义 dummyHead(虚拟头节点)、dummyTail(虚拟尾节点)两个虚拟头尾节点,避免后续处理中针对头尾节点的特殊处理;再用 length 属性维护链表长度

真实头节点:dummyHead.next
真实尾节点:dummyTail.prev

双向链表中定义 add、delete、deleteHead 方法,实现对链表的新增、删除及删除头节点功能(因为我们新增插入是在链表尾部,所以如需淘汰从链表头开始);

import lombok.Getter;
import lombok.Setter;

/**
 * 双向链表
 * LFU相同使用频次实现使用双向链表存储,头尾节点(head、tail)使用两个默认的虚拟节点,这样避免使用时针对头尾节点的特殊处理
 */
@Getter
@Setter
public class DoubleLinkedList {

    /**
     * 虚拟头节点,实际头节点应为dummyHead.next
     */
    private Node dummyHead;

    /**
     * 虚拟尾节点,实际尾节点应为dummyTail.prev
     */
    private Node dummyTail;

    /**
     * 链表长度
     */
    private int length;

    public DoubleLinkedList() {
        //默认虚拟头、尾节点初始化
        this.dummyHead = new Node();
        this.dummyTail = new Node();
        this.dummyHead.setNext(this.dummyTail);
        this.dummyTail.setPrev(this.dummyHead);
        this.length = 0;
    }

    /**
     * 添加节点
     * 每次添加节点在链表末尾
     * @param node
     */
    public void add(Node node) {
        node.setNext(this.dummyTail);
        node.setPrev(this.dummyTail.getPrev());
        this.dummyTail.getPrev().setNext(node);
        this.dummyTail.setPrev(node);
        this.length++;
    }

    /**
     * 删除节点
     * @param node
     */
    public void delete(Node node) {
        node.getPrev().setNext(node.getNext());
        node.getNext().setPrev(node.getPrev());
        node.setPrev(null);
        node.setNext(null);
        this.length--;
    }

    /**
     * 删除头节点
     */
    public void deleteHead() {
        Node head = this.dummyHead.getNext();
        if (head == null) {
            return;
        }
        this.delete(this.dummyHead.getNext());
    }

}

3.3 LFUCache 类实现

我们使用双向链表 +双哈希表的方式实现LFU。LFUCache 中定义哈希表一 nodeMap(节点Map,key为节点key,值为节点Node)、哈希表二 cntMap(使用频次Map,key为使用频次,值为相同频次节点组成的双向链表,时间最新在链尾)、capacity(容量大小)、length(当前元素数量)属性,且应包含 get、put、getMinCnt(获取最少使用频次) 方法,具体实现见下面代码

import java.util.*;
import java.util.stream.Collectors;

/**
 * LFU算法Java实现,指定容量达到时淘汰使用频次最低数据,同频次则按照LRU删除最早数据
 */
public class LFUCache {

    /**
     * 节点Map
     */
    private Map<Integer, Node> nodeMap;

    /**
     * 使用频次Map,key为使用频次,value为频次相同的双向链表,最新时间放在链表尾部,淘汰从头节点开始
     */
    private Map<Integer, DoubleLinkedList> cntMap;

    /**
     * 容量大小,超过时应淘汰最久未使用数据
     */
    private int capacity;

    /**
     * 当前元素数量
     */
    private int length;

    /**
     * 元素不存在默认为-1
     */
    private static final int NOT_FOUND = -1;

    private static final Integer ONE = 1;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.length = 0;
        nodeMap = new HashMap<>();
        cntMap = new HashMap<>();
    }

    /**
     * get获取单个节点值,未找到则返回-1
     * @param key
     * @return
     */
    public int get(int key) {
        if (this.capacity == 0) {
            //容量capacity设置为0直接返回-1
            return NOT_FOUND;
        }
        if (!nodeMap.containsKey(key)) {
            //根据key在哈希表中未找到即不存在返回-1
            return NOT_FOUND;
        }

        //节点存在
        Node node = nodeMap.get(key);
        //根据使用频次cnt获取对应的双向链表,从链表中删除当前元素
        int cnt = node.getCnt();
        cntMap.get(cnt).delete(node);

        //使用频次cnt+1,查询cntMap如链表不存在则新建,将节点插入到链表中(默认插入到链表尾部)
        cnt++;
        DoubleLinkedList linkedList = cntMap.containsKey(cnt) ? cntMap.get(cnt) : new DoubleLinkedList();
        //更新节点使用频次
        node.setCnt(cnt);
        linkedList.add(node);
        cntMap.put(cnt, linkedList);
        return node.getValue();
    }

    /**
     * put新增或更新节点
     * 注意:新增节点时如达到设定容量上限,则应淘汰使用频次最少数据,如频次相同按LFU算法方式处理
     * @param key
     * @param value
     */
    public void put(int key, int value) {
        if (this.capacity == 0) {
            //容量capacity设置为0即认为无法进行put操作,直接返回即可
            return ;
        }

        //键值对存在,即应更新维护键值对新值,且使用频次cnt+1
        if (nodeMap.containsKey(key)) {
            Node node = nodeMap.get(key);

            //根据使用频次cnt获取对应的双向链表,从链表中删除当前元素
            int cnt = node.getCnt();
            cntMap.get(cnt).delete(node);

            //使用频次cnt+1,查询cntMap如链表不存在则新建,将节点插入到链表中(默认插入到链表尾部)
            cnt++;
            DoubleLinkedList linkedList = cntMap.containsKey(cnt) ? cntMap.get(cnt) : new DoubleLinkedList();
            //更新节点使用频次、节点值
            node.setCnt(cnt);
            node.setValue(value);
            linkedList.add(node);
            cntMap.put(cnt, linkedList);
            return ;
        }

        //当容量达到上限需删除使用频次最少数据中的第一个节点
        if (this.length == this.capacity) {
            //最少使用频次
            int minCnt = LFUCache.getMinCnt(cntMap);
            DoubleLinkedList linkedList = cntMap.get(minCnt);
            //哈希表映射中删除对应映射
            nodeMap.remove(linkedList.getDummyHead().getNext().getKey());
            //链表中移除头结点元素
            linkedList.deleteHead();
            if (linkedList.getLength() == 0 && minCnt > 1) {
                //该使用频次链表无节点元素,正常应cntMap中删除该频次节点。但考虑到后续插入新元素频次固定为1,所以此逻辑加一个minCnt大于1才删除的逻辑
                cntMap.remove(minCnt);
            }
        }

        //元素未达到上限前插入动作,则length长度应递增1
        if (this.length < this.capacity) {
            this.length++;
        }

        //哈希表新增映射
        Node node = new Node(key, value);
        nodeMap.put(key, node);

        //新增元素固定频次为1
        DoubleLinkedList linkedList = cntMap.containsKey(ONE) ? cntMap.get(ONE) : new DoubleLinkedList();
        linkedList.add(node);
        cntMap.put(ONE, linkedList);
    }

    @Override
    public String toString() {
        if (this.length == 0) {
            return "LFUCache[]";
        }

        //从头节点开始遍历按顺序打印
        Set<Integer> set = cntMap.keySet();
        List<Integer> list = set.stream().collect(Collectors.toList());
        Collections.sort(list);
        StringBuffer sb = new StringBuffer("LFUCache[");
        for (int i = 0; i < list.size(); i++) {
            Integer k = list.get(i);
            DoubleLinkedList linkedList = cntMap.get(k);
            Node node = linkedList.getDummyHead().getNext();
            while (node != linkedList.getDummyTail()) {
                if (i == list.size() - 1 && node.getNext() == linkedList.getDummyTail()) {
                    //最后一个元素
                    sb.append("{\"key\": ").append(node.getKey()).append(", \"value\": ").append(node.getValue())
                            .append(", \"cnt\": ").append(node.getCnt()).append("}");
                } else {
                    sb.append("{\"key\": ").append(node.getKey()).append(", \"value\": ").append(node.getValue())
                            .append(", \"cnt\": ").append(node.getCnt()).append("}, ");
                }
                node = node.getNext();
            }
        }
        sb.append("]");
        return sb.toString();
    }

    /**
     * 获取最少使用频次
     * @param cntMap 使用频次Map,key为使用频次,value为频次相同的双向链表,时间升序排列
     * @return 最少使用频次
     */
    private static int getMinCnt(Map<Integer, DoubleLinkedList> cntMap) {
        if (cntMap == null || cntMap.size() == 0) {
            throw new IllegalArgumentException("illegal param, cntMap can not be empty.");
        }
        Set<Integer> set = cntMap.keySet();
        List<Integer> list = set.stream().collect(Collectors.toList());
        Collections.sort(list);
        for (int i = 0; i < list.size(); i++) {
            Integer minCnt = list.get(i);
            DoubleLinkedList linkedList = cntMap.get(minCnt);
            if (linkedList == null || linkedList.getLength() == 0) {
                continue;
            }
            //使用频次已升序排列,第一个链表长度大于0的即当前最小频次
            return minCnt;
        }
        //未找到最少使用频次minCnt应抛异常,正常业务场景下cntMap不为空时应必须存在最少使用频次minCnt
        throw new RuntimeException("minCnt is not exist.");
    }
    
}

3.4 测试验证

测试代码如下:

public static void main(String[] args) {
        LFUCache LFUCache = new LFUCache(2);
        LFUCache.put(1, 11);
        LFUCache.put(2, 22);
        System.out.println(LFUCache);
        System.out.println("获取节点1,值为" + LFUCache.get(1));
        System.out.println(LFUCache);

        System.out.println("更新节点2,新值为250");
        LFUCache.put(2, 250);
        System.out.println(LFUCache);
        System.out.println("达到最大容量,插入新值");
        LFUCache.put(6, 66);
        System.out.println(LFUCache);
    }

打印输出如下:

LFUCache[{"key": 1, "value": 11, "cnt": 1}, {"key": 2, "value": 22, "cnt": 1}]
获取节点1,值为11
LFUCache[{"key": 2, "value": 22, "cnt": 1}, {"key": 1, "value": 11, "cnt": 2}]
更新节点2,新值为250
LFUCache[{"key": 1, "value": 11, "cnt": 2}, {"key": 2, "value": 250, "cnt": 2}]
达到最大容量,插入新值
LFUCache[{"key": 6, "value": 66, "cnt": 1}, {"key": 2, "value": 250, "cnt": 2}]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值