文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

冷淡的面试官,让我手写LRU缓存淘汰算法打发时间!

2024-12-03 10:05

关注

本文转载自微信公众号「小郎码知答」,作者Simon郎。转载本文请联系小郎码知答公众号。    

背景

在我们这个日益追求高效的世界,我们对任何事情的等待都显得十分的浮躁,网页页面刷新不出来,好烦,电脑打开运行程序慢,又是好烦!那怎么办,技术的产生不就是我们所服务么,今天我们就聊一聊缓存这个技术,并使用我们熟知的数据结构--用链表实现LRU缓存淘汰算法。

在学习如何使用链表实现LRU缓存淘汰算法前,我们先提出几个问题,大家好好思考下,问题如下:

好了,我们带着上面的问题来学进行下面的学习。

1、什么是缓存,缓存的作用是什么?

缓存可以简单的理解为保存数据的一个副本,以便于后续能够快速的进行访问。以计算机的使用场景为例,当cpu要访问内存中的一条数据时,它会先在缓存里查找,如果能够找到则直接使用,如果没找到,则需要去内存里查找;

同样的,在数据库的访问场景中,当项目系统需要查询数据库中的某条数据时,可以先让请求查询缓存,如果命中,就直接返回缓存的结果,如果没有命中,就查询数据库, 并将查询结果放入缓存,下次请求时查询缓存命中,直接返回结果,就不用再次查询数据库。

通过以上两个例子,我们发现无论在哪种场景下,都存在这样一个顺序:先缓存,后内存;先缓存,后数据库。但是缓存的存在也占用了一部分内存空间,所以缓存是典型的以空间换时间,牺牲数据的实时性,却满足计算机运行的高效性。

仔细想一下,我们日常开发中遇到缓存的例子还挺多的。

减少与磁盘的交互

减少对数据库的查询

减少对应用服务器的请求

减少对网站的访问

2、缓存有哪些淘汰策略?

缓存的本质是以空间换时间,那么缓存的容量大小肯定是有限的,当缓存被占满时,缓存中的那些数据应该被清理出去,那些数据应该被保留呢?这就需要缓存的淘汰策略来决定。

事实上,常用的缓存的淘汰策略有三种:先进先出算法(First in First out FIFO);淘汰一定时期内被访问次数最少的页面(Least Frequently Used LFU);淘汰最长时间未被使用的页面(Least Recently Used LRU)

这些算法在不同层次的缓存上执行时具有不同的效率,需要结合具体的场景来选择。

2.1 FIFO算法

FIFO算法即先进先出算法,常采用队列实现。在缓存中,它的设计原则是:如果一个数据最先进入缓存中,则应该最早淘汰掉。

FIFO算法

新访问的数据插入FIFO队列的尾部,队列中数据由队到队头按顺序顺序移动

队列满时,删除队头的数据

2.2 LRU算法

LRU算法是根据对数据的历史访问次数来进行淘汰数据的,通常使用链表来实现。在缓存中,它的设计原则是:如果数据最近被访问过,那么将来它被访问的几率也很高。

LRU算法

2.3 LFU算法

LFU算法是根据数据的历史访问频率来淘汰数据,因此,LFU算法中的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。在缓存中,它的设计原则是:如果数据被访问多次,那么将来它的访问频率也会更高。

LFU算法

3、如何使用链表实现缓存淘汰,有什么特点,如何优化?

在上面的文章中我们理解了缓存的概念及淘汰策略,其中LRU算法是笔试/面试中考察比较频繁的,我秋招的时候,很多公司都让我手写了这个算法,为了避免大家采坑,下面,我们就手写一个LRU缓存淘汰算法。

我们都知道链表的形式不止一种,我们应该选择哪一种呢?

思考三分钟........

好了,公布答案!

事实上,链表按照不同的连接结构可以划分为单链表、循环链表和双向链表。

双向链表相较于单链表的一大优势在于:找到前驱节点的时间复杂度为O(1),而单链表只能从头节点慢慢往下找,所以仍然是O(n).而且,对于插入和删除也是有优化的。

我们可能会有问题:单链表的插入删除不是O(1)吗?

是的,但是一般情况下,我们想要进行插入删除操作,很多时候还是得先进行查找,再插入或者删除,可见其实是先O(n),再O(1)。

因为我们需要删除操作,删除一个节点不仅要得到该节点本身的指针,也需要操作其它前驱节点的指针,而双向链表能够直接找到前驱,保证了操作时间复杂度为O(1),因此使用双向链表作为实现LRU缓存淘汰算法的结构会更高效。

算法思路

维护一个双向链表,保存所有缓存的值,并且最老的值放在链表最后面。

当访问的值在链表中时:将找到链表中值将其删除,并重新在链表头添加该值(保证链表中 数值的顺序是从新到旧)

当访问的值不在链表中时:当链表已满:删除链表最后一个值,将要添加的值放在链表头 当链表未满:直接在链表头添加

3.1 LRU缓存淘汰算法

极客时间王争的《数据结构与算法之美》给出了一个使用有序单链表实现LRU缓存淘汰算法,代码如下:

  1. public class LRUBaseLinkedList { 
  2.  
  3.      
  4.     private final static Integer DEFAULT_CAPACITY = 10; 
  5.  
  6.      
  7.     private SNode headNode; 
  8.  
  9.      
  10.     private Integer length; 
  11.  
  12.      
  13.     private Integer capacity; 
  14.  
  15.     public LRUBaseLinkedList() { 
  16.         this.headNode = new SNode<>(); 
  17.         this.capacity = DEFAULT_CAPACITY; 
  18.         this.length = 0; 
  19.     } 
  20.  
  21.     public LRUBaseLinkedList(Integer capacity) { 
  22.         this.headNode = new SNode<>(); 
  23.         this.capacity = capacity; 
  24.         this.length = 0; 
  25.     } 
  26.  
  27.     public void add(T data) { 
  28.         SNode preNode = findPreNode(data); 
  29.  
  30.         // 链表中存在,删除原数据,再插入到链表的头部 
  31.         if (preNode != null) { 
  32.             deleteElemOptim(preNode); 
  33.             intsertElemAtBegin(data); 
  34.         } else { 
  35.             if (length >= this.capacity) { 
  36.                 //删除尾结点 
  37.                 deleteElemAtEnd(); 
  38.             } 
  39.             intsertElemAtBegin(data); 
  40.         } 
  41.     } 
  42.  
  43.      
  44.     private void deleteElemOptim(SNode preNode) { 
  45.         SNode temp = preNode.getNext(); 
  46.         preNode.setNext(temp.getNext()); 
  47.         temp = null
  48.         length--; 
  49.     } 
  50.  
  51.      
  52.     private void intsertElemAtBegin(T data) { 
  53.         SNode next = headNode.getNext(); 
  54.         headNode.setNext(new SNode(data, next)); 
  55.         length++; 
  56.     } 
  57.  
  58.      
  59.     private SNode findPreNode(T data) { 
  60.         SNode node = headNode; 
  61.         while (node.getNext() != null) { 
  62.             if (data.equals(node.getNext().getElement())) { 
  63.                 return node; 
  64.             } 
  65.             node = node.getNext(); 
  66.         } 
  67.         return null
  68.     } 
  69.  
  70.      
  71.     private void deleteElemAtEnd() { 
  72.         SNode ptr = headNode; 
  73.         // 空链表直接返回 
  74.         if (ptr.getNext() == null) { 
  75.             return
  76.         } 
  77.  
  78.         // 倒数第二个结点 
  79.         while (ptr.getNext().getNext() != null) { 
  80.             ptr = ptr.getNext(); 
  81.         } 
  82.  
  83.         SNode tmp = ptr.getNext(); 
  84.         ptr.setNext(null); 
  85.         tmp = null
  86.         length--; 
  87.     } 
  88.  
  89.     private void printAll() { 
  90.         SNode node = headNode.getNext(); 
  91.         while (node != null) { 
  92.             System.out.print(node.getElement() + ","); 
  93.             node = node.getNext(); 
  94.         } 
  95.         System.out.println(); 
  96.     } 
  97.  
  98.     public class SNode { 
  99.  
  100.         private T element; 
  101.  
  102.         private SNode next
  103.  
  104.         public SNode(T element) { 
  105.             this.element = element; 
  106.         } 
  107.  
  108.         public SNode(T element, SNode next) { 
  109.             this.element = element; 
  110.             this.next = next
  111.         } 
  112.  
  113.         public SNode() { 
  114.             this.next = null
  115.         } 
  116.  
  117.         public T getElement() { 
  118.             return element; 
  119.         } 
  120.  
  121.         public void setElement(T element) { 
  122.             this.element = element; 
  123.         } 
  124.  
  125.         public SNode getNext() { 
  126.             return next
  127.         } 
  128.  
  129.         public void setNext(SNode next) { 
  130.             this.next = next
  131.         } 
  132.     } 
  133.  
  134.     public static void main(String[] args) { 
  135.         LRUBaseLinkedList list = new LRUBaseLinkedList(); 
  136.         Scanner sc = new Scanner(System.in); 
  137.         while (true) { 
  138.             list.add(sc.nextInt()); 
  139.             list.printAll(); 
  140.         } 
  141.     } 

这段代码不管缓存有没有满,都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)。

3.2使用哈希表优化LRU

事实上,这个思路还可以继续优化,我们可以把单链表换成双向链表,并引入散列表。

哈希表查找较快,但是数据无固定的顺序;链表倒是有顺序之分。插入、删除较快,但是查找较慢。将它们结合,就可以形成一种新的数据结构--哈希链表(LinkedHashMap)

哈希表+双向链表

力扣上146题-LRU缓存机制刚好可以拿来练手,题图如下:

题目:

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。

LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存

int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。

void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

思路

我们的思路就是哈希表+双向链表

代码

  1. class LRUCache { 
  2.     private int capacity; 
  3.     private HashMap<Integer, ListNode> hashmap;  
  4.     private ListNode head; 
  5.     private ListNode tail; 
  6.  
  7.     private class ListNode{ 
  8.         int key
  9.         int val; 
  10.         ListNode prev; 
  11.         ListNode next
  12.         public ListNode(){   
  13.         } 
  14.         public ListNode(int keyint val){ 
  15.             this.key = key
  16.             this.val = val; 
  17.         } 
  18.     } 
  19.  
  20.     public LRUCache(int capacity) { 
  21.         this.capacity = capacity; 
  22.         hashmap = new HashMap<>(); 
  23.         head = new ListNode(); 
  24.         tail = new ListNode(); 
  25.         head.next = tail; 
  26.         tail.prev = head; 
  27.     } 
  28.  
  29.     private void removeNode(ListNode node){ 
  30.         node.prev.next = node.next
  31.         node.next.prev = node.prev; 
  32.     } 
  33.  
  34.     private void addNodeToLast(ListNode node){ 
  35.         node.prev = tail.prev; 
  36.         node.prev.next = node; 
  37.         node.next = tail; 
  38.         tail.prev= node; 
  39.     } 
  40.  
  41.     private void moveNodeToLast(ListNode node){ 
  42.         removeNode(node); 
  43.         addNodeToLast(node); 
  44.     } 
  45.      
  46.     public int get(int key) {    
  47.         if(hashmap.containsKey(key)){ 
  48.             ListNode node = hashmap.get(key); 
  49.             moveNodeToLast(node); 
  50.             return node.val; 
  51.         }else
  52.             return -1; 
  53.         } 
  54.     } 
  55.      
  56.     public void put(int keyint value) { 
  57.         if(hashmap.containsKey(key)){ 
  58.             ListNode node = hashmap.get(key); 
  59.             node.val = value; 
  60.             moveNodeToLast(node); 
  61.             return
  62.         } 
  63.         if(hashmap.size() == capacity){ 
  64.             hashmap.remove(head.next.key); 
  65.             removeNode(head.next); 
  66.         } 
  67.  
  68.         ListNode node = new ListNode(key, value); 
  69.         hashmap.put(key, node); 
  70.         addNodeToLast(node); 
  71.     } 

巨人的肩旁:

[1]数据结构与算法之美-王争

[2]力扣-LRU缓存机制(146题)

[3]https://blog.csdn.net/yangpl_tale/article/details/44998423

[4]https://leetcode-cn.com/problems/lru-cache/solution/146-lru-huan-cun-ji-zhi-ha-xi-biao-shuan-l3um/

 

来源:小郎码知答内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯