LRU算法学习笔记

LRU算法概念

LRU算法在面试中经常遇到重要性可与快排比肩
全称:Least Recently Used即最近最久未使用
主要应用场景:缓存
我们访问数据库的时候,有些数据经常被访问(热点数据),我们把这些数据放在内存中缓存下来,就不用每次都去数据库里面查找,当下一次有同样请求的时候,可以以更快的速度返回
既然是把热点数据存在缓存中,那么缓存的容量是有限的,那么容量不足的时候,我们就要考虑剔除一部分数据,那么问题就来了,我们需要考虑剔除哪一部分数据-----就有了LRU算法,剔除一些最近没有访问的数据,达到缓存数据的实时有效性的效果

关键点:

1:一个最大容量,put方法,get方法
2:要保证快,保证都是O(1)的时间复杂度
3:上一次访问的元素在第一个

/* 缓存容量为2*/
LRUCache cache = new LRuCache(2);//你可以把 cache理解成一个队列//假设左边是队头,右边是队尾
 //最近使用的排在队头,很久未使用的排在队尾
cache.put(11);
 // cache = [(1,1)]
 cache.put(22);
 // cache = [(2,2),(1,1)]
 cache.get(1); //返回1
 // cache = [(1,1),(2,2)]
/ /解释:因为最近访问了键1,所以提前至队头
cache.put( 33);
 / / cache = [(33)(11)]
//解释:缓存容量已满,需要删除内容空出置
//优先删除久未使用的数据,也就是队尾的数据
//然后把新的数据插入队头
cache.get(2);//返回-1(未找到)
 / / cache = [(33)(11)]
//解释:cache中不存在键为2的数据
cache.put(14);
 // cache = [(1,4),(3,3)]
//解释:键1已存在,把原始值1覆盖为4//不要忘了也要将键值对提前到队头

LRU特性:查找快、插入快、删除快、有顺序之分

怎么实现LRU算法?

第一反应:map,但是map不是有序的
实际结构:双向链表+散列表
通过哈希表映射到双向链表

哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap

在这里插入图片描述
借助这个结构,我们来逐一分析上面的 3 个条件:
1、如果我们每次默认从链表尾部添加元素,那么显然越靠尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的。
2、对于某一个 key,我们可以通过哈希表快速定位到链表中的节点,从而取得对应 val。

3、链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过 key 快速映射到任意一个链表节点,然后进行插入和删除

为什么是存key-value,不直接存value呢?
我觉得是因为加入容量已满,需要删除链表的最后一个节点,链表好实现,但是怎么从map里面去删除节点呢?

 map.remove(tail.key);//在hashmap中删除链表最后一个节点对应的map对象

在这里插入图片描述

为什么使用双链表:删除元素保证时间复杂度为O(1),单链表还需要查找它的前一个元素
因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。

比如在谷歌搜答案,一个问题对应一个答案,底层链表存有问题和答案的信息,HashMap里面就不用存答案了,存链表中的节点,这样就规避了链表查询慢的缺点,将查询的时间复杂度降到O(1),
在这里插入图片描述
那么链表中也没有必要记录Q了,因为HashMap里面已经记录了,可以把数据结构更新如下
在这里插入图片描述

在这里插入图片描述
我的理解:map里面的顺序还是你插入删除的顺序,只是,map里面各个key的value之间是有联系的,形成了一条链表,链表里面的顺序才是真正的顺序

在这里插入图片描述在这里插入图片描述

我的代码(反序遍历链表还有bug)

package LeetCode;
/*
 * @Author 此生辽阔
 * @Description 实现LRU算法
 * @Date 16:05 2021/3/14
 * @Param 
 * @return 
 **/

/*
遇到的问题:
怎么初始化头结点和尾节点:在调用put方法的时候初始化
怎么在其他方法里面调用map:把map定义为成员变量
map.put()的时候,key和value怎么传?所以,node要存放key和value,map.put(node.key,node)
当map.put()的时候,如何判断map中是否有此节点呢,或者只是value不同呢? map.containsKey(node.key)?
插入数据需要考虑LRU的空间是否已满
*/
import SwordOffer.deleteNode;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

public class LRU {
    
    

    private static int capacity;//LRU的容量
    public static Node head;//链表头结点
    public static Node tail;//链表尾节点
    public static Map<Integer,Node> map=new HashMap<>();//定义HashMap
    public LRU(int capacity) {
    
    
        this.capacity = capacity;
    }

    public static void main(String[] args) {
    
    
         // Node head=null;

        LRU lru=new LRU(5);

        lru.put(1,1);
       System.out.println( "lru.put(1,1)");
      printList();
      lru.put(2,2);
      System.out.println( "lru.put(2,2)");
     printList();

        lru.put(3,3);
      System.out.println( "lru.put(3,3)");
      printList();

        lru.put(4,4);
        System.out.println( "lru.put(4,4)");
        printList();

        lru.put(5,5);
        System.out.println( "lru.put(5,5)");
        printList();
        lru.put(6,6);
        System.out.println( "lru.put(6,6)");
        printList();
        lru.put(7,7);
        System.out.println( "lru.put(7,7)");
         printList();
        lru.get(3);
        System.out.println( "  lru.get(3)");

        printList();
        lru.put(8,8);
        System.out.println( "lru.put(8,8)");
        printList();
        lru.get(7);
        System.out.println( "  lru.get(7)");
        printList();
        lru.put(6,12);
        System.out.println( "lru.put(6,12)");
        printList();
       lru.get(6);
        System.out.println( "  lru.get(6)");
        printList();

       lru.get(5);
        System.out.println( "  lru.get(5)");
        printList();
        lru.put(9,9);
        System.out.println( "lru.put(9,9)");
        printList();

        System.out.println();
        for (Map.Entry<Integer, Node> entry : map.entrySet()) {
    
    
            System.out.println(entry.getKey() + ":" + entry.getValue().val);
        }

}
          static void  printList()
          {
    
    
              Node node=head;
              Node node2=tail;
              System.out.print("正序遍历链表: ");
              while(node!=null)
              {
    
    
                  if(node.next!=null)
                  {
    
    
                      System.out.print(node.val+"->");
                  }
               else
                  {
    
    
                      System.out.print(node.val);
                  }
                  node=node.next;
              }
              System.out.println();
//              System.out.print("反序遍历链表: ");
//              while(node2!=null)
//              {
    
    
//                  if(node2.pre!=null) {
    
    
//                      System.out.print(node2.val + "->");
//                  }
//                  else
//                  {
    
    
//                      System.out.print(node2.val );
//                  }
//                  node2=node2.pre;
//              }
//              System.out.println();
          }

    //思路,LRU算法的核心是当插入一个数据时,把它放到链表的头部,如果插入的节点本来就在链表中,那么先把它从原链表中删除,然后插入到链表头部
    //插入数据:
    //删除数据:
    //获取数据:
    //我们要实现LRU算法,要为LRU算法定义get方法和put方法以及delete方法
    static void  get(int key)
    {
    
    
        Node node=map.get(key);
        if(node==null)
        {
    
    
//            Logger logger = Logger.getLogger("未查询到结点");
//            logger.info("未查询到结点");
            System.out.println("未查询到结点");
            return ;//查找不到节点,直接返回
        }

        if (node==head)
        {
    
    
           //如果当前查找的节点是链表的头结点,那么直接返回
            return;
        }
        //如果当前查找的节点不是链表的头结点,需要先从链表删除节点,再在链表头部插入该节点,那么我们需要定义链表的删除和插入方法
        deleteNode( node);
        putNode( node);
    }

   // static void put(Node node)
   static void put(int key ,int value)
    {
    
    
        Node node2= map.get(key);
        if(node2!=null)
       {
    
    
           //map.get( node.key)=node.val;//更新节点的val值

          // Node node1=map.get(node.key);
         //  node1.val= node.val;
           node2.val=value;
         //  Node node= map.get(key);
        //   map.put(key, node2);//这句话是没必要的,node2就是指向map中key对应的节点
           deleteNode(node2);
           putNode(node2);
           return;
       }


//        if(map.get( node.key)!=null)//如果map里面有这个节点的key
//        {
    
    
//            map.get( node.key).val=node.val;//更新节点的val值
//          //  map.put(node.key,node);
//
//        }

           Node node=new Node(key,value);
            if(map.size()>=capacity)//如果LRU缓存还没有满
            {
    
    
                System.out.println("缓存已满,删除:"+tail.key);
                map.remove(tail.key);//在hashmap中删除链表最后一个节点对应的map对象
                deleteNode(tail);
            }


       // System.out.println("我执行了");
/*
 我们来理一下上面的逻辑,先判断map中有没有传入的节点的key,如果有,就更新对应的value
 如果没有,说明是个新节点,我们先判断LRU缓存有没有满,如果满了,就删除链表末尾的节点

 */
        if(head==null)  //如果当前链表为空
        {
    
    
            head=tail=node;//把传入的node置为头结点和尾节点
            map.put(node.key,node);
            return;
        }
        if(head==tail)//如果当前链表只有一个节点
        {
    
    

           //map中已经包含此key,就更新value
               node.next=head;
               head.pre=node;
              //head.next=tail;

               head=node;
              // tail= head.next;//这句话我不知道为什么不加。。。
               map.put(node.key,node);
               return;
        }

        //当前链表至少有两个节点
        map.put(node.key,node);
        putNode( node);

    }

    static void  deleteNode(Node node)//链表的删除节点的方法
    {
    
    
        if(node==head)//按理说,应该不用判断头结点,因为只有不是头结点的情况才会调用删除
        {
    
    
            //删除头节点
            head.next.pre=null;
            head=head.next;
            return;//进入if,执行完之后就返回,不向下执行
        }

        if(node==tail)//删除尾节点
        {
    
    
            tail.pre.next=null;
            tail=tail.pre;
            return;//进入if,执行完之后就返回,不向下执行
        }

        //删除中间节点
        node.pre.next=node.next;
        node.next.pre=node.pre;

        node.pre=null;
        node.next=null;
    }

    static void putNode(Node node)//链表插入节点的方法
    {
    
    

        node.next=head;
        head.pre=node;
        head=node;//更新当前的头结点指向
    }
}


//定义节点类
class Node{
    
    
    public int key;
    public int val;

    public Node pre;
    public Node next;

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

测试结果

lru.put(1,1)
正序遍历链表: 1
lru.put(2,2)
正序遍历链表: 2->1
lru.put(3,3)
正序遍历链表: 3->2->1
lru.put(4,4)
正序遍历链表: 4->3->2->1
lru.put(5,5)
正序遍历链表: 5->4->3->2->1
缓存已满,删除:1
lru.put(6,6)
正序遍历链表: 6->5->4->3->2
缓存已满,删除:2
lru.put(7,7)
正序遍历链表: 7->6->5->4->3
  lru.get(3)
正序遍历链表: 3->7->6->5->4
缓存已满,删除:4
lru.put(8,8)
正序遍历链表: 8->3->7->6->5
  lru.get(7)
正序遍历链表: 7->8->3->6->5
lru.put(6,12)
正序遍历链表: 12->7->8->3->5
  lru.get(6)
正序遍历链表: 12->7->8->3->5
  lru.get(5)
正序遍历链表: 5->12->7->8->3
缓存已满,删除:3
lru.put(9,9)
正序遍历链表: 9->5->12->7->8

5:5
6:12
7:7
8:8
9:9

使用LinlkedHashMap实现LRU算法

Java集合详解5:深入理解LinkedHashMap和LRU缓存

算法–用LinkedHashMap简单实现LRU缓存算法(Java实现)

以下代码参考算法题就像搭乐高:手把手带你拆解 LRU 算法

LinkedHashMap的底层就是HashMap+数组,我们只需要对这个map做查询和添加的操作,而不需要操作链表
查询:根据key查询,如果没有找到key,就返回-1,如果找到,就返回key对应的值,同时我们要把这个key移除,然后添加到map的末尾
添加:先判断map中有没有这个key,如果有,就更新value的值,同时把这个key移除,然后添加到map的末尾。如果没有这个Key,说明是新的key,我们首先判断map中的容量有没有达到缓存容量,如果没有达到,直接插入到map的末尾,如果达到缓存容量,那么需要先移除map的首元素(首元素是缓存中最老的没有访问的数),然后把这个新的key插入到map的末尾

class Lruc
{
    
    
    int cap;
    LinkedHashMap<Integer,Integer> cache=new LinkedHashMap<>();

    public Lruc(int cap) {
    
    
        this.cap = cap;
    }

    public int get (int key)
    {
    
    
        if(!cache.containsKey(key))
        {
    
    
            return -1;
        }
        // 将 key 变为最近使用
        makeRecently(key);
        return cache.get(key);
    }

    public void put(int key, int val) {
    
    
        if (cache.containsKey(key)) {
    
    

            cache.put(key, val);// 修改 key 的值
            makeRecently(key); // 将 key 变为最近使用
            return;
        }

        if (cache.size() >= this.cap) {
    
    
            // 链表头部就是最久未使用的 key
            int oldestKey = cache.keySet().iterator().next();
            cache.remove(oldestKey);
           
        }
    
        cache.put(key, val);    // 将新的 key 添加链表尾部

    }

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

参考文献

视频:田小齐详解「LRU Cache」正确的面试解答方式 Leetcode 146 刷题冲呀

算法题就像搭乐高:手把手带你拆解 LRU 算法
基于哈希表和双向链表的 LRU 算法实现
原创 | 你会用缓存吗?详解LRU缓存淘汰算法
带有过期时间的LRU实现(java版)
【系统设计】LRU缓存

猜你喜欢

转载自blog.csdn.net/ningmengshuxiawo/article/details/114627272