LRU算法

  1. LeetCode题目:
  2. 解题思路
  3. 代码:
  4. 扩展点:

LRU(Last Recently Used)算法一般用在缓存,作为一种缓存淘汰的机制,即淘汰掉最近最少使用的记录。其要求一般为:

  • 维护一个固定大小的缓存。
  • 当缓存满时,若要增加新元素,需先淘汰掉缓存中最久未使用的元素。
  • 元素最近一次访问的时间离当前越近,越不容易被淘汰。

LeetCode题目:

解题思路

  • 题目要求put()、get()操作的时间复杂度为O(1),最合适的数据结构应当为HashMap。

  • 此外,当容量满时,我们要淘汰掉最近最少使用的元素。

    我们可能会想到队列,它是一个先进先出的数据结构,用队列能很快捷地淘汰掉最老的元素,但却不能淘汰掉最近最少使用的元素!

    所以我们需要一种机制判断每个元素最近使用的频繁程度,给元素加上一个计数器么?naive!这样要花线性时间遍历才能找到最近最少使用的元素!

    其实队列已经很接近最终结果了,我们要做的只是一点点改进——每次get()获取到缓存中的值后,还要把它从队列里挖出来,再让它到队尾去重新排队,缓存满了我们就淘汰掉队头那些最近都没用到的元素。这样就很好地解决了“最近最少使用”这个问题。

  • 所以最终的解决方案为:

    HashMap<key,队列节点> + 基于双向链表的队列

    LRU缓存

  • 一个比喻:

    排队的时候,队列中某人被叫去处理点事,等回来后他就得重新排队,就是这个规矩。

代码:

要求:15min之内手写完!作为高频题目,必须多练习

/**
 *
 * 执行用时 : 18 ms , 在所有 Java 提交中击败了 97.28% 的用户
 * 内存消耗 : 47.9 MB , 在所有 Java 提交中击败了 100.00% 的用户
 */
class LRUCache {
    int capacity;
    int currentSize=0;
    HashMap<Integer,Node> map;
    Node head;
    Node tail;

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

    /**
     * 考虑情况:
     * 1. 该节点为tail,直接返回其值
     * 2. 该节点不为tail,且存在于缓存中,挪到tail后面成为新tail,返回其值
     * 3. 该节点不存在于缓存,返回-1
     */
    public int get(int key) {
        Node node=map.get(key);
        if(node!=null){
            if(node!=tail){
                moveToTail(node);
            }
            return node.val;
        }else{
            return -1;
        }
    }

    /**
     * 考虑情况:
     * 1. 缓存中存在该key,直接更新对应节点的value
     * 2. 缓存中不存在该key:
     *      若缓存已满,先从map和双向链表中淘汰掉head节点,即最近最少使用节点
     *      生成新节点node:
     *          缓存为空(刚刚初始化),tail,head指向node;
     *          缓存不为空,新节点node加入map,在链表中放到到tail后,成为新的tail
     */
    public void put(int key, int value) {
        Node exist=map.get(key);
        if(exist==null){    //缓存中不存在该 kv 对
            
            if(currentSize>=capacity){  //缓存满了,挤走head
                Node new_head=head.next;
                if(new_head!=null){
                    new_head.prev=null;
                }
                head.next=null;

                map.remove(head.key);
                head=new_head;


                currentSize--;
            }
            
            Node node=new Node(key,value);
            
            if(tail==null){ //缓存为空
                head=node;
                tail=node;
            }else{          //缓存不空

                tail.next=node;
                node.prev=tail;
                tail=node;
            }
            
            map.put(key,node);
            currentSize++;
            
        }else{
            exist.val=value;
            moveToTail(exist);
        }
    }

    /**
     * 挪到tail位置成为新tail
     * 情况分析:
     * 1. node就是tail,不做任何操作,直接返回
     * 2. node不是tail,让其前后节点互相指向,将其挪到tail后面,成为新的tail:
     *      如果node为head,head指向node_next
     *      tail指向node
     * @param node 待交换节点
     */
    private void moveToTail(Node node){
        if (node==tail)return;

        Node node_prev=node.prev;
        Node node_next=node.next;

        if(node_prev!=null){
            node_prev.next=node_next;
        }
        if(node_next!=null){
            node_next.prev=node_prev;
        }

        node.prev=tail;
        tail.next=node;
        node.next=null;

        if (head==node)head=node_next;
        tail=node;
    }
    
    class Node{
        int key;
        int val;
        Node prev;
        Node next;
        Node(int key,int val){
            this.key=key;
            this.val=val;
        }
    }
}

扩展点:

  1. 哨兵写法(dummy head、dummy tail)

  2. 如何保障线程安全?

    • 加锁:synchronize method

    • 如何保障高效(high throughput):CAS操作

    • ConcurrentHashMap(分段锁)

    • 容忍一定的读写精确性损失(读写锁)

  3. 能不能做成环形而不是双向链表?

  4. 提前分配空间(HashMap)

  5. 分布式环境下LRU怎么解决?


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,邮件至 708801794@qq.com

文章标题:LRU算法

文章字数:1k

本文作者:梅罢葛

发布时间:2020-04-08, 22:14:01

最后更新:2020-04-08, 23:58:18

原始链接:https://qiurungeng.github.io/2020/04/08/LRU%E7%AE%97%E6%B3%95/
目录
×

喜欢就点赞,疼爱就打赏