页面置换算法
页面置换算法、逐出策略、淘汰算法,其实都是比较类似的概念,都是用来处理内存中不再需要的页面,从而提高内存的利用率。
一般可以在操作系统中、虚拟内存管理中、数据库的 Buffer Pool 中、Redis 中看到页面置换算法的身影。
而常见的页面置换算法又有这几种:FIFO、LRU、LFU、Clock、OPT 等等。
这里会以面试手撕设计题的视角来讲讲这几个常见的页面置换算法,以及它们的实现方式和优化。
FIFO
FIFO 是最简单的一个,基于 First In First Out 的原则,完全符合队列的特性,因此使用 Queue 来实现。
LRU
LRU 是 Least Recently Used 的缩写,也就是最近最少使用算法,是一种常用的页面置换算法。
这里利用双向链表和哈希表来实现 LRU 算法,链表的头部是最近最久未使用,尾部是最近最常使用。双向循环链表是为了快速地插入或删除节点,哈希表是为了快速地查找获取节点。
LFU
LFU 是 Least Frequently Used 的缩写,也就是最不经常使用算法,是一种比较复杂的页面置换算,主要考虑的是使用或访问的频率。
这里 O(n)时间复杂度的方法暂且不多赘述,最容易想到的办法应该是利用优先队列(PriorityQueue)或者说堆(Heap)来实现。由于它们二者是基于完全二叉树结构的,那么 get 和 put 方法的时间复杂度都应该是 O(logn)的。
python3 中的 heapq
模块提供了堆的实现,默认是小根堆,也就是说优先 pop 最小的元素。在这个场景下,如果将(频率, 时间戳, 键)作为元素,那么会优先参考频率,如果频率相同,则参考时间戳。将频率最小的元素 pop 出,如果频率相同,则 pop 出时间戳最小的元素。
接下来 O(1)时间复杂度的方法,类似于上面讲过的 LRU 算法,利用的也是双向链表和哈希表的组合。它会将同样使用频率的节点组成一个类似 LRU 中的双向连表结构,然后优先逐出频率最低的链表节点,再选择出其中最久未使用的节点,也就是 sentinel.next
节点。