页面置换算法: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

面试题 16.25. LRU 缓存

思路

利用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

460. LFU 缓存

思路

为了高效地访问每个 \(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))
posted @ 2022-11-19 17:19  LARRY1024  阅读(450)  评论(0编辑  收藏  举报