手撕LRU算法

161 阅读2分钟

LRU 的全称是 Least Recently Used(最近最少使用),也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。

[LeetCode LRU题](146. LRU 缓存 - 力扣(LeetCode) (leetcode-cn.com))

题目要求:

  1. 题目要求让put 和 get 方法的时间复杂度为 O(1)
  2. 要求能快速插入删除

数据结构:HashMap 可以快速查找 O(1)时间,链表可以快速删除、插入。结合成哈希链表:LinkedHashMap

在哈希链表中,首部的元素即最少使用的,需要删除的,从尾部添加最近使用的。

  • get 分析:get一次,就需要把元素放在尾部
  • put 分析:put最新的元素放在尾部,如果大小超过设置的阈值,需要删除首部元素

三种方法实现:

  • 自定义哈希链表
  • 使用LinkedHashMap
  • 继承自 LinkedHashMap

方法一:

// 节点
class Node {
    public int key, val;
    public Node next, pre;

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

// 双链表
class DoubleList {
    private Node head, tail;
    private int size;

    public DoubleList() {
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        // 双链表,注意不是循环链表
        head.next = tail;
        tail.pre = head;
        size = 0;
    }
    
    // 由于只需要从末尾添加,从头删除,所以只实现addLast和removeFirst方法
    public void addLast(Node x) {
        Node pre = tail.pre;
        pre.next = x;
        x.next = tail;
        x.pre = pre;
        tail.pre = x;
        // 不要忘记了size++
        size++;
    }
    
    public Node removeFirst() {
        if (head.next == tail) return null;
        Node first = head.next;
        remove(first);
        return first;
    }
    // 删除节点。前提:这个节点是链表中必定有的。
    public void remove(Node x) {
        x.pre.next = x.next;
        x.next.pre = x.pre;
        // 不要忘记了size--
        size--;
    }
    public int size() {
        return size;
    }
}

class LRUCache {
    private HashMap<Integer, Node> map;
    private DoubleList cache;
    private int cap;
    public LRUCache(int capacity) {
        cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        makeRecently(key);
        return map.get(key).val;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            // 删除旧的,添加新的
            deleteKey(key);
            addRecently(key, value);
            return;
        }
        if (cap == cache.size()) {
            removeLeastRecently();
        }
        addRecently(key, value);
    }
    
    /* 为了防止删除或添加时忘记同时操作cache和map,实现以下方法 */
    
    /* 将某个 key 提升为最近使用的 */
    private void makeRecently(int key) {
        Node x = map.get(key);
        // 先从链表中删除这个节点
        cache.remove(x);
        // 重新插到队尾
        cache.addLast(x);
    }
    /* 添加最近使用的元素 */
    private void addRecently(int key, int val) {
        Node x = new Node(key, val);
        cache.addLast(x);
        map.put(key, x);
    }
    /* 删除某一个 key */
    private void deleteKey(int key) {
        Node x = map.get(key);
        cache.remove(x);
        map.remove(key);
    }
    /* 删除最久未使用的元素 */
    private void removeLeastRecently() {
        // 链表头部的第一个元素就是最久未使用的
        Node x = cache.removeFirst();
        // 同时别忘了从 map 中删除它的 key
        int key = x.key;
        map.remove(key);
    }
}

方法二:

class LRUCache {
    LinkedHashMap<Integer, Integer> cache;
    int cap;
    public LRUCache(int capacity) {
        cap = capacity;
        cache = new LinkedHashMap<>();
    }

    public int get(int key) {
        if (cache.containsKey(key)) {
            makeRecent(key);
            return cache.get(key);
        }
        return -1;
    }

    public void put(int key, int value) {
        if (cache.containsKey(key)) {
            cache.put(key, value);
            makeRecent(key);
            return;
        }
        if (cache.size() >= cap) {
            int oldKey = cache.keySet().iterator().next();
            cache.remove(oldKey);
        }
        cache.put(key, value);
    }
    public void makeRecent(int key) {
        Integer val = cache.get(key);
        cache.remove(key);
        cache.put(key, val);
    }
}

方法三:

class LRUCache extends LinkedHashMap<Integer, Integer> {
    int cap;
    public LRUCache(int capacity) {
        super(capacity, 0.75F, true);
        this.cap = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size()>cap;
    }
}