基于哈希链表实现LRU算法

385 阅读3分钟

所谓LRU算法,全称是Least Recently Used,其主要的思想就是: 如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。所以,当指定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

在设计数据结构的时候,基于效率的角度看,要满足算法put()、get()操作的时间复杂度都为O(1),同时为了区分最近使用和最久使用,还要保持数据的顺序。所以这个数据结构应该满足这几个特点:查找快、插入快、删除快、有顺序

在常用的几种数据结构中,哈希表是查找、插入、删除快,但是无序;双向链表是保持了有序性,但是操作很慢。因此,将两者结合起来,就组成了一个新的数据结构:哈希链表

1642660756696.jpg

针对这种数据结构,我们来分析一下LRU的整个流程

  • 1.如果每次默认从链表头部添加元素,那么显然越靠近头部的元素就越是最近使用的。越靠近尾部的元素就是越久未使用的。
  • 2.对于某一个 key ,可以通过哈希表快速定位到链表中的节点,从而取得对应的 value。
  • 3.链表显然是支持在任意位置快速插入和删除的,修改指针就行。但是链表无法按照索引快速访问某一个位置的元素,都是需要遍历链表的,所以这里借助哈希表,可以通过 key,快速的映射到任意一个链表节点,然后进行插入和删除。

代码实现:

双链表的节点类:

class Node {
    public int key, val;
    public Node next, prev;
    public Node(int k, int v) {
        this.key = k;
        this.val = v;
    }
}

然后依靠我们的 Node 类型构建一个双链表DoubleList,实现几个要用到的 API,这些操作的时间复杂度均为 O(1) :

class DoubleList {
    // 在链表头部添加节点 x
    public void addFirst(Node x){...};

    // 删除链表中的 x 节点(x 一定存在)
    public void remove(Node x){...};

    // 删除链表中最后一个节点,并返回该节点
    public Node removeLast(){...};

    // 返回链表长度
    public int size(){...};
}
    private HashMap<INteger, Node> map;
    private DoubleList cache;
    private int cap; //最大容量
    
    public LRUCache(int capacity) {
        this.cap = capacity;
        map = new HashMap<>();
        cache = new DOubleList();
    }
    
    public int get(int key) {
        if(!map.containsKey(key)) {
            return -1;
        }
        int val = map.get(key).val;
        // 利用put方法把数据提前
        put(key, val);
        return val;
    }
    
    public void put(int key, int val) {
        Node x = new Node(key, val);
        if (map.containsKey(key)) {
            // 删除旧节点,新的插入到头部
            cache.remove(map.get(key));
            cache.addFirst(x);
            //更新map中对应的数据
            map.put(key, x);
        } else {
            if(cap == cache.size()) {
                // 删除链表最后一个数据
                Node last = cache.removeLast():
                mpa.remove(last.key);
            }
            // 直接添加到头部
            cache.addFirst(x);
            map.put(key, x);
        }
    }
}

最后,解决几个疑问:

1、为什么要用双向链表?

因为涉及到删除元素的操作,且要求时间复杂度为O(1),所以需要指向前驱节点的指针。

2、哈希表中已经存了key,链表中为什么还要存key和value?

因为我们在删除链表最后一个节点后,还需要删除对应哈希表中的key。

if(cap == cache.size()) {
    // 删除链表最后一个数据
    Node last = cache.removeLast():
    mpa.remove(last.key);
}