LRU 和 LFU 的 Java 实现

/ 0评 / 0

前几天在 LeetCode 上做题时做到了与 LRU 和 LFU 相关的题目,恰逢现在是暑期实习面试的高峰,LRU 也是面试中的常考内容,所以在此对 LRU 和 LFU 的 Java 实现做一个总结。

LRU  最近最久未使用

简单介绍一下 LRU,此处搬运自百度百科:

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

顾名思义,LRU 是一种淘汰算法,具体的淘汰策略是把最近最久未使用(或者说是最近最少使用)的页面淘汰掉,LRU 算法在操作系统的内存置换,Redis 缓存的数据淘汰策略中都有所涉及。百度百科中的介绍提到了根据 t 值进行淘汰,但是在实际应用中保存 t 值并不是一件容易的事,所以我们需要借助特别的数据结构来实现 LRU 算法。

具体来说,使用一个 HashMap 和一个自定义的双向链表:链表节点存储key,value,前向指针和后向指针;HashMap 中存储 key 和对应的链表节点;双向链表代表使用顺序,越靠近头节点表示最近使用过,越靠近尾节点表示很久没有使用,作用就是模拟具体的淘汰步骤了。

双向链表节点的结构如下

class ListNode {
    int key;
    int val;
    ListNode prev;
    ListNode next;
}

LRU 的组成,我们定义 head 和 tail 作为辅助节点,表示双向链表的首尾

class LRUCache {
    Map<Integer, ListNode> cache;
    ListNode head;
    ListNode tail;
    int maxCap;

    public LRUCache(int capacity) {
        cache = new HashMap<>(capacity);
        head = new ListNode();
        tail = new ListNode();
        head.next = tail;
        tail.prev = head;
        maxCap = capacity;
    }
}

具体的操作步骤

public void put(int key, int value) {
    if (cache.containsKey(key)) { // 存在
        // 获取到 key 对应的节点
        ListNode node = cache.get(key);
        // 修改 value 值
        node.val = value;
        // 把该节点从链表上取下来
        node.prev.next = node.next;
        node.next.prev = node.prev;
        // 插入到链表头部
        addToHead(node);
    } else { // 不存在
        // 创建一个节点
        ListNode node = new ListNode();
        node.key = key;
        node.val = value;
        // 判断是否还有空间
        if (cache.size() == maxCap && maxCap != 0) {
            // 已达到最大容量,移除最近最久未使用
            ListNode popNode = popTail();
            cache.remove(popNode.key);
        }
        // 插入到链表头部
        addToHead(node);
        // 在 map 中进行记录
        cache.put(key, node);
    }
}
public int get(int key) {
    if (cache.containsKey(key)) {
        // 获取到 key 对应的节点
        ListNode node = cache.get(key);
        // 把该节点从链表上取下来
        node.prev.next = node.next;
        node.next.prev = node.prev;
        // 插入到链表头部
        addToHead(node);
        return node.val;
    }
    return -1;
}

完整的代码如下:

class LRUCache {

    class ListNode {
        int key;
        int val;
        ListNode prev;
        ListNode next;
    }

    Map<Integer, ListNode> cache;
    ListNode head;
    ListNode tail;
    int maxCap;

    public LRUCache(int capacity) {
        cache = new HashMap<>(capacity);
        head = new ListNode();
        tail = new ListNode();
        head.next = tail;
        tail.prev = head;
        maxCap = capacity;
    }

    public void addToHead(ListNode node) {
        head.next.prev = node;
        node.next = head.next;
        head.next = node;
        node.prev = head;
    }

    public ListNode popTail() {
        ListNode tmp = tail.prev;
        tmp.prev.next = tail;
        tail.prev = tmp.prev;
        return tmp;
    }

    public int get(int key) {
        if (cache.containsKey(key)) {
            ListNode node = cache.get(key);
            node.prev.next = node.next;
            node.next.prev = node.prev;
            addToHead(node);
            return node.val;
        }
        return -1;
    }

    public void put(int key, int value) {
        if (cache.containsKey(key)) {
            ListNode node = cache.get(key);
            node.val = value;
            node.prev.next = node.next;
            node.next.prev = node.prev;
            addToHead(node);
        } else {
            ListNode node = new ListNode();
            node.key = key;
            node.val = value;
            if (cache.size() == maxCap && maxCap != 0) {
                ListNode popNode = popTail();
                cache.remove(popNode.key);
            } 
            addToHead(node);
            cache.put(key, node);
        }
    }
}

LFU  最不经常使用

 

发表评论

邮箱地址不会被公开。 必填项已用*标注