页面置换算法: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表能在
代码实现
【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
思路
为了高效地访问每个
为了高效地淘汰频率低的
流程
用到的数据结构如下:
- 使用 hash 表
记录所有 键值对,保证存储和读取的时间复杂度都是 ; - 使用 hash 表
记录每一个 的访问频率(次数); - 使用 hash 表
记录每一个访问频率与该频率对应的 值列表。
查询get(key)
查询的逻辑比较简单,如果
存储put(key, value)
如果
代码实现
【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
为了简化数据结构,将上述代码中的
优化后的代码
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))
本文作者:LARRY1024
本文链接:https://www.cnblogs.com/larry1024/p/16906492.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步