算法通关村第五关——彻底弄懂LRU页面置换算法

83 阅读3分钟

黄金挑战:LRU页面置换算法

LRU算法是操作系统中常用的页面置换算法

image.png

对应leetcode146. LRU 缓存

本题LRU算法是通过HashMap+双向链表数据结构来实现的;

hashMap用于存放key,和链表中对应的节点node;

双向链表实现置换的功能;当链表中存在的节点被访问时,该节点会移动到头节点位置;如果在添加节点时出现,链表中节点大小>=capacity时;就会出现删除链表尾部节点,同时将新节点插入原头节点位置;

图解:

image.png

实现代码:

/*
 * 最近最少使用算法,使用hash+双向链表来实现;
 * */import com.demo.linkedlist.ListNode;
​
import java.util.HashMap;
import java.util.Map;
​
public class LRUCache {
    // 自定义双向链表结构;
    static class DLinkedNode {
        int key;
        int value;
        DLinkedNode next; // 指向后一个节点
        DLinkedNode prev; // 指向前一个节点
​
        public DLinkedNode() {
        }
​
        public DLinkedNode(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
​
    // cache用来存放 key,以及链表中的节点
    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private Integer size; // 当前链表中节点的个数
    private Integer capacity; // 链表允许的最大节点个数
​
    private DLinkedNode head, tail; // 双向链表的虚拟头节点和尾节点;
​
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
​
    // 用来获取缓存机制中对应key的值;
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        } else {
            moveToHead(node);
            return node.value;
        }
    }
​
    // 用于将新的key,value加入到缓存机制中;
    public void put(int key, int value) {
        // 先从map中判断当前node是否已经存放
        DLinkedNode node = cache.get(key);
        // 未存放
        if (node == null) {
            // 创建一个新的节点
            DLinkedNode curNode = new DLinkedNode(key, value);
            // 存入cache中
            cache.put(key, curNode);
            // 如果size == capacity 就说明需要移除掉链表尾部节点
            if (size >= capacity) {
                DLinkedNode tail = removeTail();
                // 将cache中的存放的尾节点删除;
                cache.remove(tail.key);
                // size--;
                size--;
            }
            // 将新节点插入链表头部
            addToHead(curNode);
            // 当前链表的节点个数+1;
            size++;
        } else {
            //说明已经链表中已经存放该节点,只需将链表中的节点移动到头部,同时替换原有的节点的值;
            node.value = value;
            moveToHead(node);
        }
    }
​
    // 头插法
    private void addToHead(DLinkedNode curNode) {
        // 让当前节点的prev指针指向head;next指针指向原来head指向的节点,即head.next;
        curNode.prev = head;
        curNode.next = head.next;
        // 原来head指向的节点的prev指针指向当前插入节点
        head.next.prev = curNode;
        // 让head的next指向当前插入节点
        head.next = curNode;
    }
​
    // 删除链表中的指定节点
    private void removeMidNode(DLinkedNode curNode) {
        curNode.next.prev = curNode.prev;
        curNode.prev.next = curNode.next;
    }
​
    // 将指定的节点移动到头结点的位置
    private void moveToHead(DLinkedNode curNode) {
        removeMidNode(curNode);
        addToHead(curNode);
    }
​
    // 移除尾部节点,需要进行返回的目的是为了删除掉cache中原有的尾节点
    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeMidNode(res);
        return res;
    }
    // 测试LRU结构
    public static void main(String[] args) {
        // 初始化一个容量为3的LRUCache结构
        LRUCache cache=new LRUCache(3);
        // 放入3个元素,此时链表中的节点应该是 (3,3) -> (2,2) -> (1,1);
        cache.put(1,1);
        cache.put(2,2);
        cache.put(3,3);
        // get(1)命中缓存,将节点(1,1)移动到头节点位置,链表变成 (1,1) -> (3,3) -> (2,2);
        int res = cache.get(1);
        // put(1,10)之后,链表节点变为 (1,10) -> (3,3) -> (2,2);
        cache.put(1,10);
        // get(2)之后,链表节点变为 (2,2) -> (1,10) -> (3,3);
        res=cache.get(2);
    }
​
}

最终测试结果如下图,链表结构为

image.png

map中存放的元素为

image.png