文章翻新 250913
LinkedList、LinkedHashMap、PriorityQueue 信息文章最初发表于 240903,原标题:手写 LRU 缓存以理解 Java 中的 LinkedHashMap,原封面。
在操作系统的学习过程中,我们会在不同的内容中(页面置换算法、高速缓冲器替换算法)接触到各种缓存置换算法:
| 算法 | 名称 | 说明 |
|---|---|---|
| OPT | 最佳置换算法 | 此算法在已知未来所有访问记录的前提下,每次都替换未来不再被访问/最远被访问的现存数据。该算法是理论上的最优算法。 |
| FIFO | 先进先出置换算法 | 每次替换最先进入缓存的数据,该算法认为最先进入的数据在将来被访问到的可能性最小。FIFO 算法存在 Belady 现象。 |
| LRU | 最近最少使用置换算法 | 每次替换最久未被访问的数据,该算法认为最近一段时间没有被访问到的数据在将来被访问的可能性最小,这种策略在实际中应用较广。 |
| LFU | 最少使用置换算法 | 每次替换访问次数最小的数据,该算法的思想是最近一段时间被访问次数最小的数据在将来被访问的可能性最小。 |
| RAND | 随机算法 | 从现存数据中随机选择一个元素进行替换,该算法不需要维护历史访问记录的任何信息,实现上简单高效,但命中率通常一般。 |
Belady 现象
Belady 现象是计算机系统中采用先进先出(FIFO)页面置换算法时出现的异常现象,表现为当分配给进程的物理页面数增加时,缺页率不降反升。贝拉迪通过实验发现,当操作系统未满足进程全部页面需求时,FIFO 算法可能因过早置换仍将被频繁访问的页面,引发更多缺页中断。这一发现打破了 「增加内存必然降低缺页率」的传统认知。
缓存替换算法用于管理有限的缓存空间,决定哪些数据需要被移除以腾出空间。现在编程题也喜欢考察对于这些算法的实现能力。
本文结合真实题目,讲解用 Java 语言实现 OPT、LRU、LFU 算法,并了解相关的 Java 数据结构。至于 FIFO、RAND 很简单,读者可自行实现。
本文题目难度标识:🟩简单,🟨中等,🟥困难。
LinkedList 双向链表从 JDK1.7 开始,
LinkedList由双向循环链表改为双向链表。
LinkedList 是一个基于双向链表实现的集合类。


一些操作的时间复杂度:
| API | 操作 | 时间复杂度 | 源码做法 |
|---|---|---|---|
add(e)addFirst(e)addLast(e) |
头尾插入 | 双向链表常规做法 | |
add(index,e) |
指定位置插入 | 选择靠近的一端向中间遍历 | |
remove()removeFirst()removeLast() |
头尾删除 | 双向链表常规做法 | |
remove(E e) |
指定元素删除(删除首次出现元素) | 从头到尾遍历链表 | |
remove(index) |
指定位置删除 | 选择靠近的一端向中间遍历 | |
getFirst()getLast() |
获取头尾元素 | 双向链表常规做法 | |
get(int index) |
获取链表指定位置的元素 | 选择靠近的一端向中间遍历 | |
clear() |
移除此链表中的所有元素 | 从头到尾遍历链表 | |
contains(e) |
查询指定元素是否存于链表 |
循环使用 remove(int index) 的时间复杂度。
Q:在一个 LinkedList 类型的变量进行 for 循环遍历时,每轮迭代执行一次 remove(index) 操作,请问该循环时间复杂度是多少?
A:,因为一次 remove(int index) 就需要 。
LinkedHashMap 含双向链表的哈希集合
LinkedHashMap 是 Java 提供的一个集合类,它继承自 HashMap,并在 HashMap 基础上维护一条双向链表。

LinkedHashMap 特性:
HashMap 的最大区别)HashMap 来说,迭代效率会高很多。由于双向链表的存在,其插入性能可能会比 HashMap 略低。LinkedHashMap 迭代顺序和插入顺序一致它的用法和 HashMap 一致。特点在于 LinkedHashMap 的从头到尾迭代顺序是和插入顺序一致的,而 HashMap 中是随机的。
accessOrderLinkedHashMap 定义了排序模式 boolean accessOrder = false。当 accessOrder 为 true 时,被访问的元素将重新拿出来放到链表尾部。
设置方式:
1 |
|
removeEldestEntry 方法removeEldestEntry 将返回一个 boolean 值,告知 LinkedHashMap 是否需要移除链表首元素。我们需要在继承 LinkedHashMap 的子类中重写该方法:
1 | public class MyMap<K, V> extends LinkedHashMap<K, V> { |
PriorityQueue 优先队列(二项堆)优先队列基于二项堆,用法详见:站内文章Java 集合的使用。
缓存替换的最优算法(MIN/OPT,Optimal)在 1966 年由 Laszlo A.Belady 提出,此算法在已知未来所有访问记录的前提下,每次都替换未来不再被访问/最远被访问的现存数据。该算法是理论上的最优算法,因为需要已知未来所有访问记录,并不具备可实现性,通常用于衡量其它缓存替换算法的优劣。
在计算机中,CPU 只能和高速缓存 Cache 直接交换数据。当所需的内存单元不在 Cache 中时,则需要从主存里把数据调入 Cache。此时,如果 Cache 容量已满,则必须先从中删除一个。
在现代计算机中,往往采用 LRU(最近最少使用) 的算法来进行 Cache 调度——可是,从上一个例子就能看出,这并不是最优的算法。 对于一个固定容量的空 Cache 和连续的若干主存访问请求,聪聪想知道如何在每次 Cache 缺失时换出正确的主存单元,以达到最少的 Cache 缺失次数。
思路:采用 OPT,即淘汰在之后的访问中,出现最晚(甚至不出现)的元素。
我的解法:
key。remove(obj) 操作可能有一定的时间复杂度。1 | import java.util.Scanner; |
最近最少使用(Least Recently Used,LRU),是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
请你设计并实现一个满足 LRU(最近最少使用)缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。void put(int key, int value)
key 已经存在,则变更其数据值 valuecapacity ,则应该 逐出 最久未使用的关键字。函数 get 和 put 必须以 的平均时间复杂度运行。
首先介绍直接使用 Java API 的写法。Java 中 LinkedHashMap 的特性使其天然地成为轻松实现 LRU 缓存的数据结构。具体实现思路如下:
LinkedHashMap;accessOrder 为 true ,这样在访问元素时就会把该元素移动到链表尾部,链表首元素就是最近最少被访问的元素;removeEldestEntry 方法,该方法会返回一个 boolean 值,告知 LinkedHashMap 是否需要移除链表首元素(缓存容量有限)。1 | class LRUCache extends LinkedHashMap<Integer,Integer>{ |
如果面试手撕环节这道题目这样写,估计面试官就会叫你在家等通知了。LinkedHashMap 无外乎就是哈希表 HashMap + 双向链表 LinkedList。把它拆开来,就会有以下实现:
HashMap + LinkedList。HashMap 用于快速定位双向链表中的结点;LinkedList 用于区分缓存 key 的访问顺序。1 | class LRUCache { |
但是,上面的实现方法中使用了 LinkedList 的 remove(obj) 操作,这个操作的时间复杂度为 ,不符合题目的时间复杂度要求。因此我们需要自己手写双向链表改进这块的时间复杂度。在接下来的实现中:
HashMap 直接定位链表中待删除的 node,优化 remove(obj) 的时间复杂度1 | class LRUCache { |
最近最不常用算法(LFU,Least Frequently Used)每次替换访问次数最小的数据,该算法的思想是最近一段时间被访问次数最小的数据在将来被访问的可能性最小。
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。void put(int key, int value) 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1(由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。
函数 get 和 put 必须以 的平均时间复杂度运行。
同 LRU 中说明的顺序一样,我们还是先从 API 用法开始介绍,再慢慢改造为手工实现。
实现要点:
HashMap,一个保存 key 对应的频数(频数用于索引链表以及快速判空),一个保存不同频数下保存的链表结构。LinkedHashMap,使用它的一些特性:
O(1) 时间复杂度1 | class LFUCache { |
接下来,我们开始用 LinkedList 替换 LinkedHashMap。变化点:
Node 结点,把频数内化到 Node 中。key2Node,要获取频数可以先得到 Node,在通过 node.freq 获取。LinkedList 进行链表操作,自行模拟链表插入、删除操作维护链表顺序。1 | class LFUCache { |
在上面的实现中,还是存在 LinkedList 的 remove(obj) 的时间复杂度过高的问题。我们需要自己写双向链表。要点:
key2Node 哈希表能在 时间内定位待删结点,降低时间复杂度。1 | class LFUCache { |
文章翻新 250913
LinkedList、LinkedHashMap、PriorityQueue 信息文章最初发表于 240903,原标题:手写 LRU 缓存以理解 Java 中的 LinkedHashMap,原封面。
在操作系统的学习过程中,我们会在不同的内容中(页面置换算法、高速缓冲器替换算法)接触到各种缓存置换算法:
| 算法 | 名称 | 说明 |
|---|---|---|
| OPT | 最佳置换算法 | 此算法在已知未来所有访问记录的前提下,每次都替换未来不再被访问/最远被访问的现存数据。该算法是理论上的最优算法。 |
| FIFO | 先进先出置换算法 | 每次替换最先进入缓存的数据,该算法认为最先进入的数据在将来被访问到的可能性最小。FIFO 算法存在 Belady 现象。 |
| LRU | 最近最少使用置换算法 | 每次替换最久未被访问的数据,该算法认为最近一段时间没有被访问到的数据在将来被访问的可能性最小,这种策略在实际中应用较广。 |
| LFU | 最少使用置换算法 | 每次替换访问次数最小的数据,该算法的思想是最近一段时间被访问次数最小的数据在将来被访问的可能性最小。 |
| RAND | 随机算法 | 从现存数据中随机选择一个元素进行替换,该算法不需要维护历史访问记录的任何信息,实现上简单高效,但命中率通常一般。 |
Belady 现象
Belady 现象是计算机系统中采用先进先出(FIFO)页面置换算法时出现的异常现象,表现为当分配给进程的物理页面数增加时,缺页率不降反升。贝拉迪通过实验发现,当操作系统未满足进程全部页面需求时,FIFO 算法可能因过早置换仍将被频繁访问的页面,引发更多缺页中断。这一发现打破了 「增加内存必然降低缺页率」的传统认知。
缓存替换算法用于管理有限的缓存空间,决定哪些数据需要被移除以腾出空间。现在编程题也喜欢考察对于这些算法的实现能力。
本文结合真实题目,讲解用 Java 语言实现 OPT、LRU、LFU 算法,并了解相关的 Java 数据结构。至于 FIFO、RAND 很简单,读者可自行实现。
本文题目难度标识:🟩简单,🟨中等,🟥困难。
LinkedList 双向链表从 JDK1.7 开始,
LinkedList由双向循环链表改为双向链表。
LinkedList 是一个基于双向链表实现的集合类。


一些操作的时间复杂度:
| API | 操作 | 时间复杂度 | 源码做法 |
|---|---|---|---|
add(e)addFirst(e)addLast(e) |
头尾插入 | 双向链表常规做法 | |
add(index,e) |
指定位置插入 | 选择靠近的一端向中间遍历 | |
remove()removeFirst()removeLast() |
头尾删除 | 双向链表常规做法 | |
remove(E e) |
指定元素删除(删除首次出现元素) | 从头到尾遍历链表 | |
remove(index) |
指定位置删除 | 选择靠近的一端向中间遍历 | |
getFirst()getLast() |
获取头尾元素 | 双向链表常规做法 | |
get(int index) |
获取链表指定位置的元素 | 选择靠近的一端向中间遍历 | |
clear() |
移除此链表中的所有元素 | 从头到尾遍历链表 | |
contains(e) |
查询指定元素是否存于链表 |
循环使用 remove(int index) 的时间复杂度。
Q:在一个 LinkedList 类型的变量进行 for 循环遍历时,每轮迭代执行一次 remove(index) 操作,请问该循环时间复杂度是多少?
A:,因为一次 remove(int index) 就需要 。
LinkedHashMap 含双向链表的哈希集合
LinkedHashMap 是 Java 提供的一个集合类,它继承自 HashMap,并在 HashMap 基础上维护一条双向链表。

LinkedHashMap 特性:
HashMap 的最大区别)HashMap 来说,迭代效率会高很多。由于双向链表的存在,其插入性能可能会比 HashMap 略低。LinkedHashMap 迭代顺序和插入顺序一致它的用法和 HashMap 一致。特点在于 LinkedHashMap 的从头到尾迭代顺序是和插入顺序一致的,而 HashMap 中是随机的。
accessOrderLinkedHashMap 定义了排序模式 boolean accessOrder = false。当 accessOrder 为 true 时,被访问的元素将重新拿出来放到链表尾部。
设置方式:
1 |
|
removeEldestEntry 方法removeEldestEntry 将返回一个 boolean 值,告知 LinkedHashMap 是否需要移除链表首元素。我们需要在继承 LinkedHashMap 的子类中重写该方法:
1 | public class MyMap<K, V> extends LinkedHashMap<K, V> { |
PriorityQueue 优先队列(二项堆)优先队列基于二项堆,用法详见:站内文章Java 集合的使用。
缓存替换的最优算法(MIN/OPT,Optimal)在 1966 年由 Laszlo A.Belady 提出,此算法在已知未来所有访问记录的前提下,每次都替换未来不再被访问/最远被访问的现存数据。该算法是理论上的最优算法,因为需要已知未来所有访问记录,并不具备可实现性,通常用于衡量其它缓存替换算法的优劣。
在计算机中,CPU 只能和高速缓存 Cache 直接交换数据。当所需的内存单元不在 Cache 中时,则需要从主存里把数据调入 Cache。此时,如果 Cache 容量已满,则必须先从中删除一个。
在现代计算机中,往往采用 LRU(最近最少使用) 的算法来进行 Cache 调度——可是,从上一个例子就能看出,这并不是最优的算法。 对于一个固定容量的空 Cache 和连续的若干主存访问请求,聪聪想知道如何在每次 Cache 缺失时换出正确的主存单元,以达到最少的 Cache 缺失次数。
思路:采用 OPT,即淘汰在之后的访问中,出现最晚(甚至不出现)的元素。
我的解法:
key。remove(obj) 操作可能有一定的时间复杂度。1 | import java.util.Scanner; |
最近最少使用(Least Recently Used,LRU),是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
请你设计并实现一个满足 LRU(最近最少使用)缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。void put(int key, int value)
key 已经存在,则变更其数据值 valuecapacity ,则应该 逐出 最久未使用的关键字。函数 get 和 put 必须以 的平均时间复杂度运行。
首先介绍直接使用 Java API 的写法。Java 中 LinkedHashMap 的特性使其天然地成为轻松实现 LRU 缓存的数据结构。具体实现思路如下:
LinkedHashMap;accessOrder 为 true ,这样在访问元素时就会把该元素移动到链表尾部,链表首元素就是最近最少被访问的元素;removeEldestEntry 方法,该方法会返回一个 boolean 值,告知 LinkedHashMap 是否需要移除链表首元素(缓存容量有限)。1 | class LRUCache extends LinkedHashMap<Integer,Integer>{ |
如果面试手撕环节这道题目这样写,估计面试官就会叫你在家等通知了。LinkedHashMap 无外乎就是哈希表 HashMap + 双向链表 LinkedList。把它拆开来,就会有以下实现:
HashMap + LinkedList。HashMap 用于快速定位双向链表中的结点;LinkedList 用于区分缓存 key 的访问顺序。1 | class LRUCache { |
但是,上面的实现方法中使用了 LinkedList 的 remove(obj) 操作,这个操作的时间复杂度为 ,不符合题目的时间复杂度要求。因此我们需要自己手写双向链表改进这块的时间复杂度。在接下来的实现中:
HashMap 直接定位链表中待删除的 node,优化 remove(obj) 的时间复杂度1 | class LRUCache { |
最近最不常用算法(LFU,Least Frequently Used)每次替换访问次数最小的数据,该算法的思想是最近一段时间被访问次数最小的数据在将来被访问的可能性最小。
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。void put(int key, int value) 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1(由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。
函数 get 和 put 必须以 的平均时间复杂度运行。
同 LRU 中说明的顺序一样,我们还是先从 API 用法开始介绍,再慢慢改造为手工实现。
实现要点:
HashMap,一个保存 key 对应的频数(频数用于索引链表以及快速判空),一个保存不同频数下保存的链表结构。LinkedHashMap,使用它的一些特性:
O(1) 时间复杂度1 | class LFUCache { |
接下来,我们开始用 LinkedList 替换 LinkedHashMap。变化点:
Node 结点,把频数内化到 Node 中。key2Node,要获取频数可以先得到 Node,在通过 node.freq 获取。LinkedList 进行链表操作,自行模拟链表插入、删除操作维护链表顺序。1 | class LFUCache { |
在上面的实现中,还是存在 LinkedList 的 remove(obj) 的时间复杂度过高的问题。我们需要自己写双向链表。要点:
key2Node 哈希表能在 时间内定位待删结点,降低时间复杂度。1 | class LFUCache { |