面试不再怕,20行Python代码帮你搞懂LRU算法

本文深入浅出地讲解了LRU(最近最少使用)缓存淘汰算法的基本原理及其应用场景,通过实例帮助读者理解如何利用Python实现高效且简洁的LRU缓存。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LRU算法在后端工程师面试中,是一个比较常出现的题目,这篇文章带大家一起,理解LRU算法,并最终用Python轻松实现一个基于LRU算法的缓存。

缓存是什么

先看一张图,当我们访问网页,浏览器会给服务器发请求,服务器会经过一系列的运算,把页面返回给浏览器。

当有多个浏览器同时访问的时候,就会在短时间内发起多个请求,而服务器对每一个请求都要进行一系列相同的操作。重复工作不仅浪费资源,还可能导致响应速度变慢。

而缓存则可以把服务器返回的页面保存下来,当有其他的浏览器再访问时候,就不必劳服务器大驾,直接由缓存返回页面。为了保证响应速度,缓存通常是基于比较昂贵的硬件,比如RAM,这就决定了我们很难用大量的缓存把所有的页面都存下来,当恰好没有缓存浏览器请求的页面时,依然需要请求服务器。由于缓存容量有限,而数据量无限(互联网每天新产生的页面数无法估计),就需要把好刚用在刀刃上,缓存那些最有用的信息。

LRU是什么

LRU是一种缓存淘汰算法(在OS中也叫内存换页算法),由于缓存空间是有限的,所以要淘汰缓存中不常用的数据,留下常用的数据,达到缓存效率的最大化。LRU就是这样一种决定“淘汰谁留下谁”的算法,LRU是Least recently used的缩写,从字面意思“最近最少使用”,我们就可以理解LRU的淘汰规则。

LRU的淘汰逻辑

我们用一张图来描述LRU的淘汰逻辑,图中的缓存是一个列表结构,上面是头结点下面是尾节点,缓存容量为8(8个小格子):

  • 有新数据(意味着数据之前没有被缓存过)时,加入到列表头
  • 缓存到达最大容量时,需要淘汰数据多出来的数据,此时淘汰列表尾部的数据
  • 当缓存中有数据被命中,则将数据移动到列表头部(相当于新加入缓存)

按上面的逻辑我们可以看到,一个数据如果经常被访问就会不断地被移动到列表头部,不会被淘汰出缓存,而越不经常访问的数据,越容易被挤出缓存。

20行Python代码实践LRU

接下来我们用Python来实现一个采用LRU算法的缓存。

从前面的文章中我们可以知道,缓存简化下来就两个功能,一个是往里装数据(缓存数据),一个是往外吐数据(命中缓存),所以我们的缓存对外只需要put和get两个接口就可以了。

按照前面的示意图,缓存内部我们只需要有一个列表(list)就可以实现LRU逻辑,不过用列表虽然能实现逻辑,但是在判断是否命中缓存时,速度可能非常慢(列表需要遍历才能知道数据有没有在里面)。在Python中,我们可以用基于hash的结构,比如字典(dict)或集合(set),来快速判断数据是否存在,解决列表实现的性能问题。但是字典和集合又是没有顺序的,如果能有一种既能排序,又是基于hash存储的数据结构,就好了。

在Python的collections包中,已经内置了这种实用的结构OrderedDict,OrderedDict是dict的子类,但是存储在内部的元素是有序的(列表的特点)。

解决了数据结构的问题,我们可以直接上手写逻辑了,代码如下:

class LRUCache:

    def __init__(self, capacity):
        self.capacity = capacity
        self.queue = collections.OrderedDict()
    
    def get(self, key):
        if key not in self.queue:
            return -1 // 要找的数据不在缓存中返回-1
        value = self.queue.pop(key) // 将命中缓存的数据移除
        self.queue[key] = value // 将命中缓存的数据重新添加到头部
        return self.queue[key]
        

    def put(self, key, value):
        if key in self.queue: // 如果已经在缓存中,则先移除老的数据
            self.queue.pop(key)
        elif len(self.queue.items()) == self.capacity:
            self.queue.popitem(last=False) // 如果不在缓存中并且到达最大容量,则把最后的数据淘汰
        self.queue[key] = value // 将新数据添加到头部
复制代码

下次面试在遇到LRU的题目,是不是就胸有成竹了?

扫码关注Python私房菜

<think>嗯,用户让我用Python写一个LRU算法。首先,我得确认LRU是什么。LRU是Least Recently Used的缩写,也就是最近最少使用算法,常用于缓存淘汰策略。当缓存满了的时候,会淘汰最久未被使用的数据。 接下来,我需要回忆一下LRU的基本实现方式。通常可以用哈希表和双向链表结合的方式,这样可以在O(1)时间内完成get和put操作。哈希表用来快速定位节点,双向链表维护访问顺序,最近访问的放在头部,最久未访问的在尾部,这样淘汰的时候直接去掉尾部节点。 不过Python里没有内置的双向链表结构,可能需要自己实现。或者有没有更简单的方式?比如用OrderedDict,因为Python的collections模块中的OrderedDict有保持插入顺序的特性,而且从Python 3.7开始,dict也保持了插入顺序。不过OrderedDict有个move_to_end方法,可以在访问某个键时将其移到末尾,这样最近使用的在尾部,最久未使用的在头部。或者是否反过来,最近使用的移到头部? 比如,当get一个键时,如果存在,就将其移到字典的末尾,表示最近使用过,这样最久未使用的留在前面。当容量超过限制时,删除第一个元素。这样的话,使用OrderedDict可能比较方便。 那具体的步骤应该是怎样的?比如,初始化容量,定义get和put方法。在get的时候,如果键存在,就move_to_end,并返回值;否则返回-1。在put的时候,如果键存在,更新值并移到末尾;如果不存在,添加新键到末尾,并检查容量是否超出,如果超出,就弹出第一个元素。 那这样的话,代码的大体结构应该是这样的。比如,用OrderedDict,当put时,如果键存在,先删除再插入到末尾,或者直接更新值并移动?需要确认OrderedDict的move_to_end方法是否在更新值时有效。或者应该先判断键是否存在,存在的话先move_to_end再更新值,或者直接替换? 或者,例如,在put操作中,如果键存在,就移动到最后,然后更新值;如果不存在,就添加到最后,并检查容量。当容量超过时,弹出第一个元素(最久未使用的)。 这样的话,代码可能看起来像这样: from collections import OrderedDict class LRUCache: def __init__(self, capacity: int): self.cache = OrderedDict() self.capacity = capacity def get(self, key: int) -> int: if key not in self.cache: return -1 self.cache.move_to_end(key) return self.cache[key] def put(self, key: int, value: int) -> None: if key in self.cache: self.cache.move_to_end(key) self.cache[key] = value if len(self.cache) > self.capacity: self.cache.popitem(last=False) 这样是否正确?测试一下。比如,容量为2,put(1,1),put(2,2),get(1)返回1,此时1被移到末尾。接着put(3,3),此时容量超过,会删除最久未使用的,即2。然后get(2)应该返回-1。这应该是对的。 但是要注意,OrderedDict的popitem(last=False)是弹出最先插入的,也就是最老的。当每次访问或者更新时,move_to_end,这样最近访问的会保持在最后,而最久未访问的在前面。这样当容量满时,删除的是最前面的。 这样实现的话,时间复杂度方面,get和put都是O(1)的,符合LRU的要求。 那用户可能需要一个这样的实现。但用户可能对双向链表的实现更感兴趣,或者更底层的实现?不过用OrderedDict的方法更简洁,Pythonic。 不过,如果用户想了解如何手动实现双向链表和哈希表的结构,可能需要更详细的代码。但考虑到Python的简洁性,可能更推荐使用OrderedDict的方式。 另外,是否需要处理线程安全的问题?一般情况下,这种问题可能不需要,除非特别说明。 所以,最终给出的代码可能如上面所示,用OrderedDict实现LRU Cache。同时需要解释每个步骤的作用,比如初始化、get和put方法的具体操作,以及如何维护最近使用的顺序。</think>以下是用Python实现的LRU算法代码及解释: ```python from collections import OrderedDict class LRUCache: def __init__(self, capacity: int): self.cache = OrderedDict() # 使用有序字典存储键值对 self.capacity = capacity # 缓存最大容量 def get(self, key: int) -> int: if key not in self.cache: return -1 # 将最近访问的键移到字典末尾 self.cache.move_to_end(key) return self.cache[key] def put(self, key: int, value: int) -> None: if key in self.cache: # 若键存在,先移到末尾再更新值 self.cache.move_to_end(key) self.cache[key] = value # 超出容量时删除最久未使用的键(字典头部) if len(self.cache) > self.capacity: self.cache.popitem(last=False) ``` **实现原理:** 1. **数据结构选择**: - 使用`OrderedDict`(有序字典)同时维护哈希表的快速访问特性和插入顺序 - 字典尾部存放最近使用的键,头部存放最久未使用的键 2. **核心操作**: - **`get`操作**: - 时间复杂度:$O(1)$ - 命中缓存时,通过`move_to_end`维护访问顺序 - **`put`操作**: - 时间复杂度:$O(1)$ - 插入新键时自动维护容量限制,通过`popitem(last=False)`移除最旧元素 **使用示例:** ```python lru = LRUCache(2) lru.put(1, 1) # 缓存 {1=1} lru.put(2, 2) # 缓存 {1=1, 2=2} print(lru.get(1)) # 输出 1(缓存变为 {2=2, 1=1}) lru.put(3, 3) # 移除键2,缓存 {1=1, 3=3} print(lru.get(2)) # 输出 -1(已被移除) ``` **算法特点:** 1. 符合LRU的核心特性: - 最近访问的数据保留在缓存中 - 缓存满时自动淘汰最久未使用的数据 2. 高效性: - 所有操作时间复杂度均为$O(1)$ 3. 空间复杂度:$O(n)$,其中$n$为缓存容量 此实现利用Python标准库的特性,在保证效率的同时具有代码简洁、易维护的特点,适合大多数应用场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值