页面置换算法:LRU和LFU
页面置换算法简介
在地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。
页面置换算法的好坏,将直接影响系统的性能,常见的页面置换算法:
- 最佳置换算法(OPT)
- 最近未使用页面置换算法(NRU):
- 先进先出置换算法(FIFO)
- 最近最久未使用算法(LRU)
- 最少使用置换算法(LFU)
一个好的页面置换算法,应做到减少页面置换的频率,尽量将以后不会用到的或较长时间不会使用的页面给置换出。
下面,我们主要介绍一下应用比较广泛的页面置换算法:LRU 和 LFU 算法。
LRU和LFU算法
它们的区别如下:
- LRU:最近最少使用(最长时间)淘汰算法(Least Recently Used),LRU会淘汰最长时间没有被使用的页面。
- LFU:最不经常使用(最少次)淘汰算法(Least Frequently Used),LFU会淘汰一段时间内使用次数最少的页面。
它们的应用场景:
- LRU:消耗CPU资源较少,适合较大的文件比如游戏客户端(最近加载的地图文件);
- LFU:消耗CPU资源较多,适合较小的文件和零碎的文件比如系统文件、应用程序文件 。
算法实现
我们分别通过两道力扣上的算法题,来介绍 LRU 和 LFU 算法。
LRU算法
题目:Leetcode.16.25
思路
利用Hash表和双向链表维护所有的节点数据,Hash表能在\(O(1)\)的时间内查找数据,双向链表能在\(O(1)\)时间内进行数据插入和删除。
代码实现
【Java实现】
class LRUCache {
private Map<Integer, Node> map;
private DeList cache;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
cache = new DeList();
}
public int get(int key) {
if (!map.containsKey(key)) {
return -1;
}
// 如果值存在,需要将其提升为最近使用的元素
makeRecently(key);
return map.get(key).val;
}
public void put(int key, int value) {
if (map.containsKey(key)) {
// 删除旧值
deleteKey(key);
// 在链表尾部添加一个新元素
addRecently(key, value);
return ;
}
// 判断是否需要移除头部的元素
if (capacity == cache.size()) {
removeLeastRecently();
}
// 在链表尾部添加一个新元素
addRecently(key, value);
}
// 将某个key提升为最近使用的
private void makeRecently(int key) {
Node node = map.get(key);
cache.remove(node);
cache.addLast(node);
}
// 添加最近使用的元素
private void addRecently(int key, int val) {
Node node = new Node(key, val);
cache.addLast(node);
map.put(key, node);
}
// 删除一个key
private Node deleteKey(int key) {
Node node = map.get(key);
map.remove(node);
cache.remove(node);
return node;
}
// 删除最久未使用的key
private void removeLeastRecently() {
Node node = cache.removeFirst();
map.remove(node.key);
}
// 双向链表的节点
class Node {
public int key, val;
public Node next, prev;
public Node (int key, int val) {
this.key = key;
this.val = val;
}
}
// 双向链表
class DeList {
private Node head, tail;
private int size;
public DeList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
size = 0;
}
// 在链表尾部添加一个节点
public void addLast(Node node) {
node.prev = tail.prev;
node.next = tail;
tail.prev.next = node;
tail.prev = node;
size++;
}
// 移除一个节点
public Node remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
size--;
return node;
}
// 移除链表头部的节点
public Node removeFirst() {
if (head.next == tail) {
return null;
}
return remove(head.next);
}
public int size() {
return size;
}
}
}
【Python实现】
class DLinkedNode(object):
def __init__(self, key: int = 0, value: int = 0):
self.key = key
self.value = value
self.next = None
self.prev = None
class LRUCache:
def __init__(self, capacity: int):
self.cache = dict()
self.head = DLinkedNode()
self.tail = DLinkedNode()
self.head.next = self.tail
self.tail.prev = self.head
self.capacity = capacity
self.size = 0
@staticmethod
def _remove_node(node: DLinkedNode):
""" 双向链表删除一个节点 """
node.next.prev = node.prev
node.prev.next = node.next
return node
def _add_to_head(self, node: DLinkedNode):
""" 将一个节点插入到双向链表的头部 """
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
return
def _remove_tail(self) -> DLinkedNode:
""" 删除双向链表尾部的元素 """
return self._remove_node(self.tail.prev)
def _move_to_head(self, node: DLinkedNode):
""" 将一个任意节点移动到双向链表头部 """
self._remove_node(node)
self._add_to_head(node)
def get(self, key: int) -> int:
if key not in self.cache:
return -1
node = self.cache.get(key)
self._move_to_head(node)
return node.value
def put(self, key: int, value: int) -> None:
if key not in self.cache:
new_node = DLinkedNode(key, value)
self.cache.update({key: new_node})
self._add_to_head(new_node)
self.size += 1
if self.size > self.capacity:
old_node = self._remove_tail()
self.cache.pop(old_node.key)
self.size -= 1
else:
node = self.cache.get(key)
node.value = value
self._move_to_head(node)
LFU算法
LFU算法相当于淘汰 访问频率最低 的数据。访问频率,在代码实现的时候,可以将其转换为 访问次数。
题目:Leetcode.460
思路
为了高效地访问每个 \(key\) 对应的 \(value\) ,很容易想到使用 hash 表记录每一个键值对,同时,使用用一个 hash 表记录每一个 \(key\) 的记录访问频率。
为了高效地淘汰频率低的 \(key\) ,我们还需要将每一个访问频率对应的 \(key\) 值记录下来,需要注意的是,如果多个 \(key\) 的访问频率相等时,优先淘汰最早的 \(key\) ,所以,我们需要使用有序集合存储 \(key\) 的顺序。
流程
用到的数据结构如下:
- 使用 hash 表 \(keyToValue\) 记录所有 \(key - value\) 键值对,保证存储和读取的时间复杂度都是\(O(1)\);
- 使用 hash 表 \(keyToFreq\) 记录每一个 \(key\) 的访问频率(次数);
- 使用 hash 表 \(freqToKeys\) 记录每一个访问频率与该频率对应的 \(key\) 值列表。
查询get(key)
查询的逻辑比较简单,如果 \(key\) 不在\(keyToValue\) 中,就返回 \(-1\),否则,就增加key的访问次数加1,并返回对应的value。
存储put(key, value)
如果 \(key\) 不在 \(keyToValue\) 中,就将其保存到 \(keyToValue\) 中,并将访问次数加 \(1\),否则,就判断 \(keyToValue\) 是否超过缓存的最大容量,超过就要删除最少访问次数的 \(key\) 释放一个空间,用于保存新的 \(key\) 。
代码实现
【Python实现】
from collections import defaultdict
class LFUCache(object):
def __init__(self, capacity: int):
self._key_to_value = dict()
self._key_to_freq = dict()
self._freq_to_keys = defaultdict(list)
self._min_freq = 0
self._capacity = capacity
def increase_freq(self, key: int):
freq = self._key_to_freq.get(key)
# 更新使用频率
self._key_to_freq.update({key: freq + 1})
self._freq_to_keys[freq].remove(key)
# 新的频率中增加这个key
self._freq_to_keys[freq + 1].append(key)
if len(self._freq_to_keys[freq]) == 0:
self._freq_to_keys.pop(freq)
if freq == self._min_freq:
self._min_freq += 1
def remove_min_freq_key(self):
keys = self._freq_to_keys[self._min_freq]
# 淘汰最先被插入的key
deleted_key = keys.pop(0)
if len(keys) == 0:
self._freq_to_keys.pop(self._min_freq)
# 更新key-value表
self._key_to_value.pop(deleted_key)
self._key_to_freq.pop(deleted_key)
def get(self, key: int) -> int:
if key not in self._key_to_value:
return -1
self.increase_freq(key)
return self._key_to_value.get(key)
def put(self, key: int, value: int):
if self._capacity <= 0:
return
if key in self._key_to_value:
self._key_to_value.update({key: value})
self.increase_freq(key)
return
if self._capacity <= len(self._key_to_value):
self.remove_min_freq_key()
self._key_to_value.update({key: value})
self._key_to_freq.update({key: 1})
self._freq_to_keys[1].append(key)
self._min_freq = 1
为了简化数据结构,将上述代码中的 \(value\) 和 \(freq\) 封装为一个 \(Node\) 对象,这样操作起来更简单、更通用一些,也支持其他的数据类型。
优化后的代码
from collections import defaultdict
class Node(object):
def __init__(self, value: int = None, freq: int = 1):
self.value = value # 记录节点的值
self.freq = freq # 记录节点访问的次数
def increase_frequency(self):
self.freq += 1
def get_frequency(self):
return self.freq
def __str__(self):
return "%s" % (self.value)
class LFUCache(object):
def __init__(self, capacity: int):
# 记录键值对
self._key_to_value = dict()
# 记录每一个频率对应的key值集合
self._freq_to_keys = defaultdict(list)
# 记录缓存中最小访问频率
self._min_freq = 0
# 缓存大小
self._capacity = capacity
def increase_freq(self, key: int):
# 获取当前key的访问次数
freq = self._key_to_value.get(key).get_frequency()
# 将其访问次数加1
self._key_to_value.get(key).increase_frequency()
# 从旧的访问频率中删除这个key
self._freq_to_keys[freq].remove(key)
# 新的频率中增加这个key
self._freq_to_keys[freq + 1].append(key)
# 如果旧的频率里面没有key了,及时清除这个频率
if len(self._freq_to_keys[freq]) == 0:
self._freq_to_keys.pop(freq)
# 如果删除的频率是最小的,还需要将最小频率加1
if freq == self._min_freq:
self._min_freq += 1
def remove_min_freq_key(self):
# 获取最小频率对应的key值集合
keys = self._freq_to_keys[self._min_freq]
# 淘汰最先被插入的key
deleted_key = keys.pop(0)
# 如果没有其他key值了,就将该频率删除
if len(keys) == 0:
self._freq_to_keys.pop(self._min_freq)
# 更新key-value表
self._key_to_value.pop(deleted_key)
def get(self, key: int) -> int:
# 如果key没有,就返回-1
if key not in self._key_to_value:
return -1
# 增加该key的访问次数
self.increase_freq(key)
# 返回对应的值
return self._key_to_value.get(key)
def put(self, key: int, value: int):
# 如果缓存为负数则退出
if self._capacity <= 0:
return
# 如果key已经存在
if key in self._key_to_value:
# 更新key的值
self._key_to_value.update({key: value})
# 并增加该key的访问次数
self.increase_freq(key)
return
# key是第一次加入,且缓存已经满了,就需要删除一个最少访问次数的key
if self._capacity <= len(self._key_to_value):
self.remove_min_freq_key()
# 保存新加入的key-value对,并将最小访问次数置为1
self._key_to_value.update({key: Node(value)})
self._freq_to_keys[1].append(key)
self._min_freq = 1
if __name__ == "__main__":
lfu = LFUCache(2)
print(lfu.put(1, 1))
print(lfu.put(2, 2))
print(lfu.get(1))
print(lfu.put(3, 3))
print(lfu.get(2))
print(lfu.get(3))
print(lfu.put(4, 4))
print(lfu.get(1))
print(lfu.get(3))
print(lfu.get(4))