手写LRU算法

96 阅读3分钟

LRU算法介绍

LRU算法就是最近最少使用,可以理解为在最近一段时间里,元素按照访问的时间戳进行排序,比如时间戳最大的排序越靠前(也可能越靠后),比如元素A的访问时间为00:21,元素B的访问时间为00:31,那么排序时元素B就比元素A的排序更靠前。
介绍完LRU,再介绍下LFU算法以做区分,LFU是最近最不常用,可以理解为在最近一段时间内,元素按照访问次数(频率)排序,访问频率越多排序越靠前。

LinkeHashMap介绍

LinkeHashMap就是哈希链表,是双向链表和哈希表的结合,哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢,而哈希链表兼具了哈希表和链表的优势:能快速查找,插入删除快,而且元素有序

image.png

LinkeHashMap具有两个属性:

  1. 顺序插入
    LinkedHashMap 默认按照插入顺序迭代元素。支持遍历时会按照插入顺序有序进行迭代。 默认构造函数构造出来的LinkeHashMap就是按照插入顺序排序,比如:

    HashMap < String, String > map = new LinkedHashMap < > ();
    
  2. 访问有序
    LinkedHashMap 可以按照元素访问顺序排序,这点可以被用于LRU算法。 那么怎么让LinkedHashMap按照元素访问顺序排序呢?可以看下它提供的一个构造函数

/**  
* Constructs an empty {@code LinkedHashMap} instance with the  
* specified initial capacity, load factor and ordering mode.  
*  
* @param initialCapacity the initial capacity  
* @param loadFactor the load factor  
* @param accessOrder the ordering mode - {@code true} for  
* access-order, {@code false} for insertion-order  
* @throws IllegalArgumentException if the initial capacity is negative  
* or the load factor is nonpositive  
*/  
public LinkedHashMap(int initialCapacity,  float loadFactor,  boolean accessOrder) {  
      super(initialCapacity, loadFactor);  
      this.accessOrder = accessOrder;  
}

LinkedHashMap 定义了排序模式 accessOrder(boolean 类型,默认为 false),访问顺序则为 true,插入顺序则为 false。

为了实现访问顺序遍历,我们可以使用传入 accessOrder 属性的 LinkedHashMap 构造方法,并将 accessOrder 设置为 true,表示其具备访问有序性,比如:

LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, 0.75f, true);

LinkedHashMap怎么实现访问排序:
访问一个元素这个概念不够具体,更详细的定义是插入一个新元素或者访问一个已有元素都属于访问了一个元素。LinkedHashMap是一个链表,当插入一个新元素时,会追加在链表表尾;当访问一个已有元素时,会将这个元素取出放在链表尾部。而LinkedHashMap迭代元素都是从表头遍历到表尾,这样就实现了访问排序,最近使用的排在链表尾,久未使用的排在链表头。

使用LinkeHashMap实现LRU算法

class LRUCache {

    LinkedHashMap<Integer,Integer> map;
    int capacity;

    public LRUCache(int capacity) {
        map = new LinkedHashMap<>(capacity,0.75f,true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        return map.getOrDefault(key,-1);
    }
    
    public void put(int key, int value) {
        if (map.containsKey(key)){
            map.put(key,value);
            return;
        }else {
            map.put(key,value);
            if (map.size() > capacity){
                //将表头元素移除
                int firstKey = map.keySet().iterator().next();
                map.remove(firstKey);
            }
            return;
        }
    }
}

不使用LinkeHashMap实现LRU算法

要做到查询时间复杂度为O(1), 可以使用哈希表。要做到删除增加时间复杂度也为O(1),需要双向链表。

class LRUCache {

    private class Node {
        int key, val;
        Node pre, next;
        private Node(int key, int val){
            this.key = key;
            this.val = val;
        }
    }

    private class DoubleList {
        Node head = new Node(0,0);
        Node tail = new Node(0,0);
        int size;

        private DoubleList(){
            head.next = tail;
            tail.pre = head;
            size = 0;
        }

        private void addFirst(Node node){
            Node headNext = head.next;
            head.next = node;
            headNext.pre = node;
            node.pre = head;
            node.next = headNext;
            size++;
        }

        private void remove(Node node){
            node.pre.next = node.next;
            node.next.pre = node.pre;
            size--;
        }

        private Node removeLast(){
            Node last = tail.pre;
            remove(last);
            return last;
        }

        private int size(){
            return size;
        }
    }


    // 哈希表:key -> Node(key,val)
    private Map<Integer, Node> map;

    // 双向链表:node(k1, v1) <-> Node(k2, v2)...
    private DoubleList cache;

    private int capacity;

    public LRUCache(int capacity){
        this.capacity = capacity;
        map = new HashMap<>(capacity);
        cache = new DoubleList();
    }

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

    public void put(int key, int value){
        Node node = new Node(key, value);
        if (map.containsKey(key)){
            cache.remove(map.get(key));
            cache.addFirst(node);
            map.put(key,node);
        }else {
            map.put(key,node);
            cache.addFirst(node);
            if (cache.size() > capacity){
                Node last = cache.removeLast();
                map.remove(last.key);
            }
            
        }
    }
}