【❤重点❤】【数据结构系列】——LRU缓存淘汰算法

146. LRU 缓存机制

LRU缓存淘汰算法

LRU 算法实际上是让你设计数据结构:

首先要接收一个 capacity 参数作为缓存的最大容量, 然后实现两个 API, 一个是 put(key, val) 方法存入键值对, 另一个是 get(key) 方法获取 key 对应的 val, 如果 key 不存在则返回-1。

LRU 缓存算法的核心数据结构就是哈希链表, 双向链表和哈希表的结合体。

cache这个数据结构必备条件:

  1. 显然cache中的元素必须有时序,以区分最近使用的和长久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置
  2. 要在cache中快速找某个key是否存在并得到对应的val
  3. 每次访问cache中的某个key,需要将这个元素变为最近使用的,也就是说cache要支持在任意位置快速插入和删除元素。

在这里插入图片描述
结合这个数据结构,分析上述三个条件:

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

问题:

  • 为什么是双向链表,单向链表不行吗?
    因为删除一个节点不仅要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证时间复杂度为O(1)

  • 既然哈希表已经存在了key,为什么链表中还要存key和val,只有val不行吗
    因为在移除最少使用的元素中,需要删除的节点,而不是节点的值。
    也就是说,当缓存容量已满,不仅要删除最后一个Node节点,还要把map中映射到该节点的key同时删除,而这个key只能由Node得到。如果Node结构中只存储val,那么无法得知key是什么,也就无法删除map中的键,造成错误。

双链表的节点类:

 /**
     * 定义双链表的节点类
     */
    class Node{
        public int key,val;
        public Node next,prev;
        public Node(int k,int v){
            this.key = k;
            this.val = v;
        }
    }

依靠Node类型构建双链表:

  /**
     * 依靠Node构建一个双链表
     */
    class DoubleList{
        //头尾虚节点
        private Node head,tail;
        //链表元素数
        private int size;

        public DoubleList(){
            //初始化双向链表的数据
            head = new Node(0,0);
            tail = new Node(0,0);
            head.next = tail;
            tail.prev = head;
            size = 0;
        }

        //在链表尾部添加节点x,时间复杂度为O(1)
        public void addLast(Node x){
            x.prev = tail.prev;
            x.next = tail;
            tail.prev.next = x;
            tail.prev = x;
            size++;
        }
        //删除链表中x节点(x一定存在)
        //由于是双链表且给的是目标Node节点,时间复杂度为O(1)
        public void remove(Node x){
            x.prev.next = x.next;
            x.next.prev = x.prev;
            size--;
        }
        //删除链表中第一个节点,并返回该节点,时间复杂度为O(1)
        public Node removeFirst(){
            if (head.next == tail)
                return null;
            Node first = head.next;
            remove(first);
            return first;
        }
        //返回链表长度,时间复杂度O(1)
        public int size(){
            return size;
        }

    }

把双向链表和哈希表结合起来,并实现LRU的 get() 和 put() 方法:

/**
     * 构建LRU cache,把双向链表和哈希表结合起来
     */
    class LRUCache{
        //key -> Node(key,val)
        private HashMap<Integer,Node> map;
        //Node(k1,v1) <-> Node(k2,v2)....
        private DoubleList cache;
        //最大容量
        private int cap;
        public LRUCache(int capacity){
            this.cap = capacity;
            map = new HashMap<>();
            cache = new DoubleList();
        }
        /**
         * 将某个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中添加key的映射
            map.put(key,x);
        }
        /**
         * 删除某一个key
         */
        private void deleteKey(int key){
            Node x = map.get(key);
            //从链表中删除
            cache.remove(x);
            //从map中删除
            map.remove(key);
        }
        /**
         * 删除最久未使用的元素
         */
        private void removeLeastRecently(){
            //链表头部的第一个元素就是最久未使用的
            Node deleteNode = cache.removeFirst();
            //别忘了从map中删除它的key
            map.remove(deleteNode.key);
        }
        /**
         * 实现LRU的get方法
         */
        public int get(int key){
            if (!map.containsKey(key)){
                return -1;
            }
            //将数据提升为最近使用的
            makeRecently(key);
            return map.get(key).val;
        }
        /**
         * 实现 LRU的put方法
         */
        public void put(int key,int val){
            if (map.containsKey(key)){
                //删除旧的数据
                deleteKey(key);
                //新插入的数据为最近使用的数据
                addRecently(key,val);
                return;
            }
            //如果容量已满
            if (cap == cache.size){
                //删除最久未使用的元素
                removeLeastRecently();
            }
            //添加为最近使用的元素
            addRecently(key,val);
        }

    }

使用java内置类型LinkedHashMap来实现LRU算法

package labuladong_learn.Data_structure;/**
 * Copyright (C), 2019-2021
 * author  candy_chen
 * date   2021/4/9 10:49
 *
 * @Classname LRU_2
 * Description: 使用java内置类型LinkedHashMap来实现LRU算法
 */

import java.util.LinkedHashMap;

/**
 *
 */
public class LRU_2 {
    static class LRUCache{
        int cap;
        LinkedHashMap<Integer,Integer> cahce = new LinkedHashMap<>();
        public LRUCache(int capacity){
            this.cap = capacity;
        }
        public int get(int key){
            if (!cahce.containsKey(key)){
                return -1;
            }
            //将key变为最近使用
            makeRecently(key);
            return cahce.get(key);
        }

        public void put(int key,int val){
            if (cahce.containsKey(key)){
                //修改key值
                cahce.put(key,val);
                //将key变为最近使用
                makeRecently(key);
                return;
            }
            if (cahce.size() >= this.cap){
                //链表头部就是最久未使用的key
                int oldestKey = cahce.keySet().iterator().next();
                cahce.remove(oldestKey);
            }
            //将新的key添加到链表尾部
            cahce.put(key,val);
        }

        private void makeRecently(int key) {
            int val = cahce.get(key);
            //删除key,重新插入到队尾
            cahce.remove(key);
            cahce.put(key,val);
        }
    }
}

posted @ 2021-04-09 11:18  your_棒棒糖  阅读(98)  评论(0)    收藏  举报