LRU算法Java实现

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

一、前言

LRU(The Least Recently Used)即最近最少使用算法。其原理是如果一个数据最近没有被使用,那么将来它被使用的可能也更低,因此在内存达到一定阈值时,将最近最少使用的数据淘汰的一种策略算法。

二、图解

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

三、LRU 算法 Java 实现

Java实现LRU算法可通过哈希表+双向链表的方式实现,在哈希表中维护所有节点,实现查找的时间复杂度为O(1)(实际元素出现哈希碰撞后,形成的链表或者红黑树时间复杂度为O(logN)此处不考虑,只当哈希表为O(1)))。

3.1 定义节点类

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

import lombok.Getter;
import lombok.Setter;

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

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

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

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

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

    public Node() {
    }

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

}

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

3.2 双向链表实现

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

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

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

import lombok.Getter;
import lombok.Setter;

/**
 * 双向链表
 * LRU实现使用双向链表存储,头尾节点(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 LRUCache 类实现

即使用我们上方定义的双向链表(DoubleLinkedList )+ 哈希表(HashMap)实现LRU。LRUCache 中定义哈希表 nodeMap(节点Map)、双向链表 linkedList、capacity(容量大小)属性,且应包含 get、put 方法,具体实现见下面代码

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

/**
 * LRU Java实现,指定容量,达到时淘汰最久未使用数据
 * 通过双向链表+哈希表的方式实现查找、插入、删除的复杂度为O(1)
 */
public class LRUCache {

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

    /**
     * 双向链表
     */
    private DoubleLinkedList linkedList;

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

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

    public LRUCache(int capacity) {
        this.capacity = capacity;
        nodeMap = new HashMap<>();
        linkedList = new DoubleLinkedList();
    }

    /**
     * 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);
        //get获取某个节点,即最新使用应将节点移动到双向链表尾部,可认为是删除、插入链表的一个组合操作
        this.linkedList.delete(node);
        this.linkedList.add(node);
        return node.getValue();
    }

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

        if (nodeMap.containsKey(key)) {
            //键值对存在,即应更新维护键值对新值,且将该节点移动到链表尾部
            Node node = nodeMap.get(key);
            this.linkedList.delete(node);
        } else if (this.capacity == this.linkedList.getLength()) {
            //容量达到上限,删除链表中头节点,且从哈希表中也要移除映射
            Node head = this.linkedList.getDummyHead().getNext();
            nodeMap.remove(head.getKey());
            this.linkedList.deleteHead();
        }
        //重新维护哈希表映射,新增节点(默认移动到尾部)
        Node node = new Node(key, value);
        nodeMap.put(key, node);
        this.linkedList.add(node);
    }

    @Override
    public String toString() {
        if (this.linkedList.getLength() == 0) {
            return "LRUCache{}";
        }

        //从头节点开始遍历按顺序打印
        Node node = this.linkedList.getDummyHead().getNext();
        StringBuffer sb = new StringBuffer("LRUCache{");
        while (node != this.linkedList.getDummyTail()) {
            if (node.getNext() == this.linkedList.getDummyTail()) {
                //最后一个元素
                sb.append(node.getKey() + ": " + node.getValue());
            } else {
                sb.append(node.getKey() + ": " + node.getValue() + ", ");
            }
            node = node.getNext();
        }
        sb.append("}");
        return sb.toString();
    }
    
}

3.4 测试验证

测试代码如下:

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

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

打印输出如下:

LRUCache{1: 11, 2: 22}
获取节点1,值为11
LRUCache{2: 22, 1: 11}
更新节点2,新值为250
LRUCache{1: 11, 2: 250}
达到最大容量,插入新值
LRUCache{2: 250, 6: 66}

四、使用LinkedHashMap实现LRU

LinkedHashMap 内部本身就很好的支持了LRU算法,可通过继承 LinkedHashMap 实现 LRU,只要定义 capacity(容量大小),且覆盖父类 removeEldestEntry 方法,定义删除最早元素触发逻辑(this.size() > this.capacity)即可;

import lombok.Getter;
import java.util.LinkedHashMap;
import java.util.Map;

@Getter
public class LRUCache<K, V> extends LinkedHashMap {

    /**
     * 容量大小
     */
    private int capacity;

    public LRUCache(int capacity) {
        super();
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
    	//链表长度达到上限,触发删除链表最早未使用元素
        return this.size() > this.capacity;
    }
    
}
好的,下面是一个简单的 Java 实现,使用 LRU(Least Recently Used)算法计算页面置换的缺页率和缺页序列: ```java import java.util.*; public class LRUPageReplacement { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.print("请输入内存块数:"); int numFrames = sc.nextInt(); int[] frames = new int[numFrames]; Arrays.fill(frames, -1); // 初始化为无效页 Map<Integer, Integer> pageLastUsed = new HashMap<>(); // 记录页最近使用时间 int numFaults = 0; // 缺页次数 List<Integer> pageFaults = new ArrayList<>(); // 记录缺页序列 System.out.print("请输入页面引用串(以空格分隔):"); String[] refStr = sc.next().split("\\s+"); for (String ref : refStr) { int page = Integer.parseInt(ref); boolean found = false; for (int i = 0; i < numFrames; i++) { if (frames[i] == page) { found = true; break; } } if (!found) { int leastUsedPage = -1; long minLastUsed = Long.MAX_VALUE; for (int i = 0; i < numFrames; i++) { int f = frames[i]; long lastUsed = pageLastUsed.getOrDefault(f, 0); if (lastUsed < minLastUsed) { leastUsedPage = i; minLastUsed = lastUsed; } } frames[leastUsedPage] = page; pageLastUsed.put(page, System.nanoTime()); numFaults++; pageFaults.add(page); } else { pageLastUsed.put(page, System.nanoTime()); } } double faultRate = (double) numFaults / refStr.length; System.out.printf("缺页率:%.2f%%\n", faultRate * 100); System.out.println("缺页序列:" + pageFaults); } } ``` 运行程序,输入内存块数和页面引用串,即可输出缺页率和缺页序列。例如,输入: ``` 请输入内存块数:3 请输入页面引用串(以空格分隔):1 2 3 4 1 2 5 1 2 3 4 5 ``` 输出: ``` 缺页率:33.33% 缺页序列:[1, 2, 3, 4, 5, 1, 2, 5, 3, 4] ``` 请注意,这只是一个简单的实现,可能存在性能问题和边界情况未处理。在实际应用中,需要根据具体情况进行优化和改进。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值