首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | 为什么要学习数据结构和算法?
02 | 如何抓住重点,系统高效地学习数据结构与算法?
03 | 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?
04 | 复杂度分析(下):浅析最好、最坏、平均、均摊时间复杂度
05 | 数组:为什么很多编程语言中数组都从0开始编号?
06 | 链表(上):如何实现LRU缓存淘汰算法?
07 | 链表(下):如何轻松写出正确的链表代码?
08 | 栈:如何实现浏览器的前进和后退功能?
09 | 队列:队列在线程池等有限资源池中的应用
10 | 递归:如何用三行代码找到“最终推荐人”?
11 | 排序(上):为什么插入排序比冒泡排序更受欢迎?
12 | 排序(下):如何用快排思想在O(n)内查找第K大元素?
13 | 线性排序:如何根据年龄给100万用户数据排序?
14 | 排序优化:如何实现一个通用的、高性能的排序函数?
15 | 二分查找(上):如何用最省内存的方式实现快速查找功能?
16 | 二分查找(下):如何快速定位IP对应的省份地址?
17 | 跳表:为什么Redis一定要用跳表来实现有序集合?
18 | 散列表(上):Word文档中的单词拼写检查功能是如何实现的?
19 | 散列表(中):如何打造一个工业级水平的散列表?
20 | 散列表(下):为什么散列表和链表经常会一起使用?
21 | 哈希算法(上):如何防止数据库中的用户信息被脱库?
22 | 哈希算法(下):哈希算法在分布式系统中有哪些应用?
23 | 二叉树基础(上):什么样的二叉树适合用数组来存储?
24 | 二叉树基础(下):有了如此高效的散列表,为什么还需要二叉树?
25 | 红黑树(上):为什么工程中都用红黑树这种二叉树?
26 | 红黑树(下):掌握这些技巧,你也可以实现一个红黑树
27 | 递归树:如何借助树来求解递归算法的时间复杂度?
28 | 堆和堆排序:为什么说堆排序没有快速排序快?
29 | 堆的应用:如何快速获取到Top 10最热门的搜索关键词?
30 | 图的表示:如何存储微博、微信等社交网络中的好友关系?
31 | 深度和广度优先搜索:如何找出社交网络中的三度好友关系?
32 | 字符串匹配基础(上):如何借助哈希算法实现高效字符串匹配?
33 | 字符串匹配基础(中):如何实现文本编辑器中的查找功能?
34 | 字符串匹配基础(下):如何借助BM算法轻松理解KMP算法?
35 | Trie树:如何实现搜索引擎的搜索关键词提示功能?
36 | AC自动机:如何用多模式串匹配实现敏感词过滤功能?
37 | 贪心算法:如何用贪心算法实现Huffman压缩编码?
38 | 分治算法:谈一谈大规模计算框架MapReduce中的分治思想
39 | 回溯算法:从电影《蝴蝶效应》中学习回溯算法的核心思想
40 | 初识动态规划:如何巧妙解决“双十一”购物时的凑单问题?
41 | 动态规划理论:一篇文章带你彻底搞懂最优子结构、无后效性和重复子问题
42 | 动态规划实战:如何实现搜索引擎中的拼写纠错功能?
43 | 拓扑排序:如何确定代码源文件的编译依赖关系?
44 | 最短路径:地图软件是如何计算出最优出行路径的?
45 | 位图:如何实现网页爬虫中的URL去重功能?
46 | 概率统计:如何利用朴素贝叶斯算法过滤垃圾短信?
47 | 向量空间:如何实现一个简单的音乐推荐系统?
48 | B+树:MySQL数据库索引是如何实现的?
49 | 搜索:如何用A*搜索算法实现游戏中的寻路功能?
50 | 索引:如何在海量数据中快速查找某个数据?
51 | 并行算法:如何利用并行处理提高算法的执行效率?
52 | 算法实战(一):剖析Redis常用数据类型对应的数据结构
53 | 算法实战(二):剖析搜索引擎背后的经典数据结构和算法
54 | 算法实战(三):剖析高性能队列Disruptor背后的数据结构和算法
55 | 算法实战(四):剖析微服务接口鉴权限流背后的数据结构和算法
56 | 算法实战(五):如何用学过的数据结构和算法实现一个短网址系统?
当前位置:
首页>>
技术小册>>
数据结构与算法之美
小册名称:数据结构与算法之美
### 06 | 链表(上):如何实现LRU缓存淘汰算法? 在探讨数据结构与算法的美妙之处时,链表作为一种基础而灵活的数据结构,其应用之广泛令人叹为观止。特别是在实现缓存机制时,链表与哈希表的结合使用能够高效解决许多实际问题,其中最为典型的便是LRU(Least Recently Used,最近最少使用)缓存淘汰算法的实现。本章将深入剖析LRU缓存淘汰算法的原理,并通过链表(特别是双向链表)来展示其高效实现方式。 #### 一、LRU缓存淘汰算法概述 在缓存管理中,由于缓存空间有限,当缓存达到容量上限时,需要按照一定的策略淘汰部分数据,以腾出空间供新数据使用。LRU缓存淘汰算法是一种常用的页面置换算法,其核心思想是:当缓存空间不足时,优先淘汰那些最长时间未被访问的数据项。这种策略基于一个假设:如果一个数据项最近被访问过,那么它将来被再次访问的可能性也较高。 #### 二、LRU缓存淘汰算法的实现挑战 要实现LRU缓存淘汰算法,主要面临以下几个挑战: 1. **快速访问**:缓存的主要目的是提高数据访问速度,因此实现方式必须支持快速的数据查找。 2. **高效更新**:每次数据访问后,都需要更新其在缓存中的位置,以反映其最近被访问的状态。 3. **有序淘汰**:当缓存空间不足时,能够迅速定位到最久未使用的数据项并将其淘汰。 #### 三、双向链表在LRU缓存中的应用 为了解决上述挑战,我们可以使用双向链表(Doubly Linked List)结合哈希表(Hash Table)来构建LRU缓存。双向链表的优势在于其支持快速的插入和删除操作,而哈希表则提供了接近O(1)时间复杂度的数据访问能力。 - **双向链表**:用于记录数据的访问顺序,即最近最少使用的数据位于链表的一端(通常是头部),而最近最常使用的数据则位于另一端(通常是尾部)。 - **哈希表**:用于存储键值对及其对应的链表节点指针,以实现快速的数据访问。 #### 四、LRU缓存的具体实现 ##### 1. 数据结构定义 首先,定义双向链表节点的数据结构: ```python class ListNode: def __init__(self, key=None, value=None): self.key = key self.value = value self.prev = None self.next = None ``` 接着,定义LRU缓存类: ```python class LRUCache: def __init__(self, capacity: int): self.capacity = capacity self.cache = {} # 哈希表 # 使用哑节点简化边界条件处理 self.head = ListNode() self.tail = ListNode() self.head.next = self.tail self.tail.prev = self.head def get(self, key: int) -> int: # 访问数据 if key not in self.cache: return -1 node = self.cache[key] self._move_to_tail(node) # 标记为最近访问 return node.value def put(self, key: int, value: int) -> None: # 插入或更新数据 if key in self.cache: node = self.cache[key] node.value = value self._move_to_tail(node) # 更新访问顺序 else: new_node = ListNode(key, value) self.cache[key] = new_node self._add_node(new_node) # 添加到链表尾部 if len(self.cache) > self.capacity: self._pop_head() # 淘汰最久未使用的数据 def _move_to_tail(self, node): # 将节点移动到链表尾部 prev = node.prev next_node = node.next prev.next = next_node next_node.prev = prev self._add_node(node) # 重新添加到尾部 def _add_node(self, node): # 在链表尾部添加节点 node.prev = self.tail.prev node.next = self.tail self.tail.prev.next = node self.tail.prev = node def _pop_head(self): # 弹出链表头部节点 if self.head.next is self.tail: return None # 链表为空 pop_node = self.head.next self.head.next = pop_node.next pop_node.next.prev = self.head del self.cache[pop_node.key] # 同时从哈希表中删除 return pop_node ``` ##### 2. 算法分析 - **时间复杂度**:`get` 和 `put` 操作的时间复杂度均为O(1),这是因为哈希表提供了快速的数据访问能力,而双向链表的插入和删除操作也是O(1)的。 - **空间复杂度**:O(capacity),其中capacity是缓存的容量,即哈希表和链表所占用的最大空间。 #### 五、LRU缓存的应用场景 LRU缓存淘汰算法因其高效性和简单性,在多种场景下得到了广泛应用,包括但不限于: - **页面置换**:在操作系统中,LRU算法被用于管理内存页面,以提高内存利用率和减少页面置换的开销。 - **缓存系统**:在Web服务器、数据库等系统中,LRU缓存用于存储热点数据,减少对磁盘或数据库的访问,提高系统响应速度。 - **分布式缓存**:在Redis、Memcached等分布式缓存系统中,虽然它们内部并不直接采用LRU算法,但提供了类似的功能,用于淘汰长时间未使用的缓存项。 #### 六、总结 通过本章的学习,我们深入理解了LRU缓存淘汰算法的原理及其基于双向链表和哈希表的实现方式。这种结合数据结构的巧妙设计,不仅解决了缓存管理中的关键问题,还展示了数据结构与算法在解决实际问题时的强大威力。在未来的编程实践中,不妨多思考如何灵活运用这些基础数据结构,以解决更加复杂和多变的问题。
上一篇:
05 | 数组:为什么很多编程语言中数组都从0开始编号?
下一篇:
07 | 链表(下):如何轻松写出正确的链表代码?
该分类下的相关小册推荐:
数据结构与算法(上)
业务开发实用算法精讲
编程之道-算法面试(下)
编程之道-算法面试(上)
数据结构与算法(中)
算法面试通关 50 讲
数据结构与算法(下)