行,我直接给你一版“可当笔记+可当实现模板+可当面试答案”的压缩版,全塞满但保证结构清晰。你可以一屏一屏滚着读。 --- # 一、问题模型(统一抽象,后面所有算法共用) **页面置换问题 = 给定引用串 + 固定物理页框数 → 最小化缺页次数** * 输入: * `pages[]`:页面访问序列(虚拟页号) * `frames`:物理页框数(内存容量) * 输出: * `page_faults`:缺页次数 * 核心状态: * `memory`:当前在内存中的页集合 * 替换策略:当 `memory` 满时,决定踢谁 --- # 二、FIFO(First-In First-Out,最简单但很蠢) ## 思想 谁最早进来谁先滚(队列) ## 特点 * 实现极简单 * 会出现 **Belady异常(页框增加反而缺页更多)** ## 数据结构 * 队列 `queue` * 集合 `set`(O(1)判断是否命中) ## 过程 ``` 访问 page: if page 在 memory: 命中 else: 缺页 if memory 未满: 加入 queue + set else: victim = queue.pop_front() 删除 victim 插入新 page ``` ## C++实现 ```cpp int FIFO(const vector& pages, int capacity) { unordered_set memory; queue q; int faults = 0; for (int p : pages) { if (memory.count(p)) continue; faults++; if (memory.size() == capacity) { int victim = q.front(); q.pop(); memory.erase(victim); } memory.insert(p); q.push(p); } return faults; } ``` --- # 三、OPT(Optimal,理论最优,用来对比) ## 思想 淘汰 **未来最久不会再用** 的页 ## 特点 * 最优(理论下界) * 不可实现(需要预知未来) ## 数据结构 * 当前内存 `memory` * 每次缺页 → 扫描未来找“最晚使用”的 ## 过程核心 ``` for 每个 page: if miss: if 未满: 加入 else: 对 memory 中每个页: 找它下一次出现的位置 选“最远”或“不再出现”的 ``` ## C++实现 ```cpp int OPT(const vector& pages, int capacity) { vector memory; int faults = 0; for (int i = 0; i < pages.size(); i++) { int p = pages[i]; if (find(memory.begin(), memory.end(), p) != memory.end()) continue; faults++; if (memory.size() < capacity) { memory.push_back(p); } else { int idx = -1, farthest = i; for (int j = 0; j < memory.size(); j++) { int k; for (k = i + 1; k < pages.size(); k++) { if (pages[k] == memory[j]) break; } if (k > farthest) { farthest = k; idx = j; } } memory[idx] = p; } } return faults; } ``` --- # 四、LRU(Least Recently Used,最重要) ## 思想 淘汰 **最近最久没用** 的页(用“过去”预测“未来”) ## 特点 * 实际系统常用 * 不会出现 Belady 异常 * 需要维护“访问顺序” --- ## 实现1:链表 + 哈希(标准解) ### 数据结构 * `list`:维护访问顺序(最新在前) * `unordered_map`:页 → 迭代器 ### 操作复杂度 * 全部 O(1) ### C++实现 ```cpp int LRU(int capacity, const vector& pages) { list lru; unordered_map::iterator> pos; int faults = 0; for (int p : pages) { if (pos.count(p)) { lru.erase(pos[p]); } else { faults++; if (lru.size() == capacity) { int victim = lru.back(); lru.pop_back(); pos.erase(victim); } } lru.push_front(p); pos[p] = lru.begin(); } return faults; } ``` --- ## 实现2:时间戳(简单但慢) ```cpp int LRU_slow(const vector& pages, int capacity) { vector memory; unordered_map last_used; int faults = 0; for (int i = 0; i < pages.size(); i++) { int p = pages[i]; if (find(memory.begin(), memory.end(), p) != memory.end()) { last_used[p] = i; continue; } faults++; if (memory.size() < capacity) { memory.push_back(p); } else { int lru_page = memory[0]; for (int m : memory) { if (last_used[m] < last_used[lru_page]) lru_page = m; } replace(memory.begin(), memory.end(), lru_page, p); } last_used[p] = i; } return faults; } ``` --- # 五、LFU(Least Frequently Used) ## 思想 淘汰访问次数最少的页 ## 问题 * “历史包袱”:旧热点永远不走 * 需要加 aging(衰减) --- ## 基本实现(简化版) ```cpp int LFU(const vector& pages, int capacity) { unordered_map freq; vector memory; int faults = 0; for (int p : pages) { if (find(memory.begin(), memory.end(), p) != memory.end()) { freq[p]++; continue; } faults++; if (memory.size() < capacity) { memory.push_back(p); } else { int victim = memory[0]; for (int m : memory) { if (freq[m] < freq[victim]) victim = m; } replace(memory.begin(), memory.end(), victim, p); freq.erase(victim); } freq[p] = 1; } return faults; } ``` --- # 六、Clock(Second Chance,操作系统最常用) ## 思想(LRU的近似) * 每页有一个 **引用位 R** * 扫描环: * R=1 → 置0,跳过(给第二次机会) * R=0 → 淘汰 ## 数据结构 * 环形数组 * `ref_bit[]` * 指针 `hand` --- ## 过程 ``` while true: if ref_bit[hand] == 0: 替换 break else: ref_bit[hand] = 0 hand++ ``` --- ## C++实现 ```cpp int Clock(const vector& pages, int capacity) { vector memory(capacity, -1); vector ref(capacity, 0); int hand = 0, faults = 0; for (int p : pages) { bool hit = false; for (int i = 0; i < capacity; i++) { if (memory[i] == p) { ref[i] = 1; hit = true; break; } } if (hit) continue; faults++; while (true) { if (ref[hand] == 0) { memory[hand] = p; ref[hand] = 1; hand = (hand + 1) % capacity; break; } else { ref[hand] = 0; hand = (hand + 1) % capacity; } } } return faults; } ``` --- # 七、算法对比(你考试/面试直接用) | 算法 | 是否最优 | 是否可实现 | 时间复杂度 | 是否Belady异常 | | ----- | ----- | ------- | ----- | ---------- | | FIFO | 否 | 是 | O(1) | 会 | | OPT | 是 | 否 | O(n²) | 不会 | | LRU | 近似最优 | 是 | O(1) | 不会 | | LFU | 不稳定 | 是 | O(n) | 不一定 | | Clock | LRU近似 | 是(OS常用) | O(1) | 不会 | --- # 八、核心理解(你真正该掌握的) 你别死记算法,要抓住这条主线: **页面置换的本质 = 用过去信息预测未来访问** * FIFO:完全不预测 → 垃圾 * OPT:完美预测 → 理论 * LRU:用“最近”预测 → 工程最佳 * Clock:LRU的低成本近似 → OS实际用 * LFU:用“频率”预测 → 容易翻车 --- # 九、你如果继续往下学(很关键) 你下一步应该看: * 工作集模型(Working Set) * 局部性原理(Temporal / Spatial Locality) * 页抖动(Thrashing) * Linux实际实现(active/inactive list,本质是LRU变种) --- 如果你要,我下一步可以直接给你: **“Linux 内核到底怎么做页面置换(不是课本版,是源码级逻辑)”** 那个才是真正有含金量的版本。 --- **LRU**(Least Recently Used,最近最少使用)是一种非常经典的**缓存淘汰算法**。 它的核心思想基于“时间局部性”原理:**如果一个数据在最近一段时间内没有被访问过,那么在将来它被访问的可能性也比较小。** 因此,当缓存空间存满时,我们会优先清理掉那个“最久未被访问”的数据。 --- ### 1. LRU 的工作机制 想象一个有固定容量的队列,数据流的操作遵循以下规则: 1. **访问数据**: * 如果数据已经在缓存中(命中),则将其移动到队列的**最前端**(表示它刚被用过)。 * 如果数据不在缓存中(未命中),将其插入到队列的**最前端**。 2. **淘汰数据**: * 当新数据进入且缓存已满时,直接丢弃队列**末尾**的数据,因为它就是那个“最久没用过”的对象。 --- ### 2. 工业级实现:哈希表 + 双向链表 在实际编程中(如 Redis 或操作系统内存管理),为了达到 $O(1)$ 的操作效率,通常采用 **Hash Map + Doubly Linked List** 的组合结构: * **双向链表 (Double Linked List)**:负责维护数据的**访问顺序**。 * 靠近 `Head` 的是最近访问的。 * 靠近 `Tail` 的是最久未访问的。 * **哈希表 (Hash Map)**:负责**快速定位**。 * Key 是数据的标识,Value 是指向链表节点的**指针/引用**。这样不需要遍历链表就能找到数据。 #### 为什么不用普通数组或单链表? * **数组**:虽然查询快,但移动元素(把中间的挪到开头)需要 $O(n)$,代价太高。 * **单链表**:删除末尾节点时,无法在 $O(1)$ 时间内找到前驱节点。 --- ### 3. LRU 的优缺点 | 优点 | 缺点 | | :--- | :--- | | **性能均衡**:对具有时间局部性的数据流非常高效。 | **缓存污染**:如果偶尔发生一次大规模的“全表扫描”,会导致热点数据全部被踢出。 | | **实现简单**:逻辑清晰,是面试和系统设计中的首选。 | **无法感知频率**:一个数据即便访问了 1000 次,只要它比只访问 1 次的数据更早被访问,它也会被淘汰。 | --- ### 4. 代码实现参考 (Rust) 考虑到你对系统级语言的偏好,这里给出一个简单的逻辑结构示例: ```rust use std::collections::HashMap; struct LRUCache { capacity: usize, // 实际生产中通常使用双向链表配合 Raw Pointer 或 RefCell // 这里示意:Map 存 Key -> (Value, Access_Timestamp) } impl LRUCache { fn get(&mut self, key: i32) -> Option { // 1. 在 Map 中找 // 2. 如果找到,更新该节点的顺序(移到链表头) // 3. 返回值 None } fn put(&mut self, key: i32, value: i32) { // 1. 如果 Key 存在,更新 Value 并移到头 // 2. 如果不存在,插入新节点到头 // 3. 如果超过 capacity,删除链表尾部节点,并同步从 Map 中移除 } } ``` ### 下一步建议 LRU 虽然经典,但在面对高并发或特定访问模式时有其局限性。你想了解它的改进版 **LRU-K**(解决缓存污染问题)或者像 Linux 内核中使用的 **Clock 算法**(更节省内存的近似 LRU 实现)吗?