LRU和LFU 算法(页面置换算法)

LRU和LFU的区别

LRU和LFU都是内存管理的页面置换算法。

LRU:最近最少使用(最长时间)淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的页面。

LFU:最不经常使用(最少次)淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面。

  • 例子

    假设LFU方法的时期T为10分钟,访问如下页面所花的时间正好为10分钟,内存块大小为3。若所需页面顺序依次如下:

    2 1 2 1 2 3 4

    ---------------------------------------->

    • 当需要使用页面4时,内存块中存储着1、2、3,内存块中没有页面4,就会发生缺页中断,而且此时内存块已满,需要进行页面置换。
    • 若按LRU算法,应替换掉页面1。因为页面1是最长时间没有被使用的了,页面2和3都在它后面被使用过。
    • 若按LFU算法,应换页面3。因为在这段时间内,页面1被访问了2次,页面2被访问了3次,而页面3只被访问了1次,一段时间内被访问的次数最少。

    LRU 关键是看页面最后一次被使用到发生替换的时间长短,时间越长,页面就会被置换;

    LFU关键是看一定时间段内页面被使用的频率(次数),使用频率越低,页面就会被置换。

  • LRU算法适合:较大的文件比如游戏客户端(最近加载的地图文件);

  • LFU算法适合:较小的文件和零碎的文件比如系统文件、应用程序文件 ;

  • LRU消耗CPU资源较少,LFU消耗CPU资源较多。

LRU (最长时间)

最近最久未使用算法, LRU是淘汰最长时间没有被使用的页面

功能

  1. 缓存容量capacity为正整数,缓存的key、value均为int类型
  2. 读缓存func get(key int) int
    • key已存在,返回对应value
    • key不存在,返回-1
  3. 写缓存func put(key int, value int):
    • key已存在,修改对应value
    • key不存在,写入该组缓存,若写入前缓存容量已达上限,则应淘汰最久未使用的缓存(强调:读、写缓存均视为使用)

数据结构

  • 使用双向链表维护缓存的上一次使用时间:

    • 约定:链表正方向(从头部到尾部)节点按照使用时间排序——越早使用(即久未使用)的节点,越靠近链表尾部
    • 维护:每使用一次缓存,就将该缓存对应的链表节点移动到链表头部;缓存淘汰时,只需要删除尾部节点即可
  • 增加一个map,记录key链表节点的映射关系; 解决如果只使用双向链表,每次判断key是否存在时,都要遍历链表

  1. cache:map[int]*listNodekey到节点的映射; 其中 listNode data:key, value
  2. list:*listNode,双向链表,维护缓存的上一次使用时间
  3. capacity:int,链表容量

伪代码

  • 读缓存
    1. key存在:
      • 在原链表中删除该缓存节点,重新插入到链表头部,
      • 返回对应的value
    2. key不存在:
      • 返回-1
  • 写缓存(更新缓存)
    1. Key存在:
      • 更新缓存节点的value值
      • 在原链表中删除该缓存节点,并把该重新插入到链表头部
    2. Key不存在:
      1. 容量已达上限:
        • 在链表中删除尾部节点(记录该节点的key)
        • 根据上一步中记录的key,删除对应的映射关系
        • 根据输入参数构造新的节点:
        • 将新的节点插入链表头部
        • 新增key到新的节点的映射关系
      2. 容量未达上限:
        • 根据输入参数构造新的节点:
        • 将新的节点插入链表头部
        • 新增key到新的节点的映射关系

Golang代码实现

// 双向链表节点
type doublyListNode struct {
    key   int
    value int
    prev  *doublyListNode
    next  *doublyListNode
}

// 构造一个双向空链表(首尾几点都是空节点)
func newDoublyList() *doublyListNode {
    headNode := &doublyListNode{}
    tailNode := &doublyListNode{}
    headNode.next = tailNode
    tailNode.prev = headNode
    return headNode
}

// 把节点添加到链表头部
func (dl *doublyListNode) addToHead(node *doublyListNode) {
    dl.next.prev = node
    node.next = dl.next
    dl.next = node
    node.prev = dl
}

// 删除链表中的节点
func removeNode(node *doublyListNode) {
    node.next.prev = node.prev
    node.prev.next = node.next
}

// LRUCache 具体的缓存
type LRUCache struct {
    cache    map[int]*doublyListNode
    head     *doublyListNode
    tail     *doublyListNode
    capacity int
}

// Constructor 构建缓存容器
func Constructor(capacity int) LRUCache {
    dl := newDoublyList()
    return LRUCache{
        cache:    make(map[int]*doublyListNode),
        head:     dl,
        tail:     dl.next,
        capacity: capacity,
    }
}

func (lruCache *LRUCache) Get(key int) int {
    // 根据key 获取缓存
    v, ok := lruCache.cache[key]
    // 如果没有缓存, 返回-1
    if !ok {
        return -1
    }
    // 如果有缓存
    removeNode(v)              // 移除该缓存
    lruCache.head.addToHead(v) // 把缓存添加双向链表头部
    return v.value
}

// Put 新建缓存
func (lruCache *LRUCache) Put(key int, value int) {
    // 已经有缓存
    if v, ok := lruCache.cache[key]; ok { // v 是双链表中的节点
        v.value = value            // 更新链表节点中的值
        lruCache.cache[key] = v    // 更新缓存中映射关系
        removeNode(v)              // 移除该缓存
        lruCache.head.addToHead(v) // 把缓存添加双向链表头部
        return
    }
    // 缓存超长 淘汰缓存
    if len(lruCache.cache) >= lruCache.capacity {
        node := lruCache.tail.prev
        removeNode(node)                 // 删除该节点
        delete(lruCache.cache, node.key) // 清除 最近最少使用的缓存
    }
    newNode := &doublyListNode{
        key:   key,
        value: value,
    }
    lruCache.cache[key] = newNode
    lruCache.head.addToHead(newNode)
}

LFU (最少次)

功能

  1. 缓存容量capacity、缓存的key和value均为自然数(可以为0,代码中单独处理)
  2. 读缓存func get(key int) int:(与lru相同)
    • key已存在,返回对应value
    • key不存在,返回-1
  3. 写缓存func put(key int, value int):
    • key已存在,修改对应value
    • key不存在,写入该组缓存,若写入前缓存容量已达上限,则应淘汰使用次数最少的缓存(记其使用次数为n);
    • 若使用次数为n的缓存数大于一个,则淘汰最久未使用的缓存(即,此时遵守lru规则)

数据结构

// LFUCache 具体的缓存  frequency 是使用次数
type LFUCache struct {
    recent   map[int]*doublyListNode // frequency 到使用次数为 frequency 的节点中,最近使用的一个的映射
    count    map[int]int             // frequency 到对应频率的节点数量的映射
    cache    map[int]*doublyListNode // key到节点的映射
    list     *doublyList             // 双向链表,维护缓存的使用次数(优先)和上一次使用时间
    capacity int                     // 容量
}

伪代码

  • 读缓存
    1. 存在:(记节点frequency为n)
      • 若存在其他frequency = n+1的节点,则将节点移动到所有frequency = n+1的节点的前面;
      • 否则,若存在其他frequency = n的节点,且当前节点不是最近节点,则将节点移动到所有frequency = n的节点的前面;
      • 否则,不移动节点(该情况下,节点就应该呆在它现在的位置)
      • 更新recent
      • 更新count
      • 将节点frequency +1
      • 返回节点的value
    2. 不存在:返回-1
  • 写缓存
    • key存在
      • 参考读缓存——key存在,额外修改对应的value即可
    • 不存在:
      • 若当前缓存容量已达上限:
        • 淘汰尾部的缓存节点(记节点freq为n)
        • 若不存在其他freq = n的节点,则将recent置空
        • 更新cache
        • 更新count
      • 构造新节点:key,value,frequency = 1
        • 是否存在其他frequency = 1的节点:
        • 存在:插入到它们的前面
        • 不存在:插入链表尾部
        • 更新recent
        • 更新cache
        • 更新count

Golang代码实现

// 双向链表
type doublyList struct {
    head *doublyListNode
    tail *doublyListNode
}

// 删除尾结点
func (dl *doublyList) removeTail() {
    pre := dl.tail.prev.prev
    pre.next = dl.tail
    dl.tail.prev = pre
}

// 链表是否为空
func (dl *doublyList) isEmpty() bool {
    return dl.head.next == dl.tail
}

// 双向链表节点
type doublyListNode struct {
    key       int
    value     int
    frequency int // 使用次数
    prev      *doublyListNode
    next      *doublyListNode
}

// 在某一个节点之前插入一个节点
func addBefore(currNode *doublyListNode, newNode *doublyListNode) {
    pre := currNode.prev
    pre.next = newNode
    newNode.next = currNode
    currNode.prev = newNode
    newNode.prev = pre
}

// LFUCache 具体的缓存
type LFUCache struct {
    recent   map[int]*doublyListNode // frequency 到使用次数为 frequency 的节点中,最近使用的一个的映射
    count    map[int]int             // frequency 到对应频率的节点数量的映射
    cache    map[int]*doublyListNode // key到节点的映射
    list     *doublyList             // 双向链表,维护缓存的使用次数(优先)和上一次使用时间
    capacity int                     // 容量
}

func removeNode(node *doublyListNode) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

// Constructor 构建缓存容器
func Constructor(capacity int) LFUCache {
    return LFUCache{
        recent:   make(map[int]*doublyListNode),
        count:    make(map[int]int),
        cache:    make(map[int]*doublyListNode),
        list:     newDoublyList(),
        capacity: capacity,
    }
}

func newDoublyList() *doublyList {
    headNode := &doublyListNode{}
    tailNode := &doublyListNode{}
    headNode.next = tailNode
    tailNode.prev = headNode
    return &doublyList{
        head: headNode,
        tail: tailNode,
    }
}

func (lfu *LFUCache) Get(key int) int {
    if lfu.capacity == 0 {
        return -1
    }
    node, ok := lfu.cache[key]
    if !ok { // key不存在
        return -1
    }
    // key已存在
    next := node.next
    if lfu.count[node.frequency+1] > 0 {
        // 存在其他使用次数为n+1的缓存,将指定缓存移动到所有使用次数为n+1的节点之前
        removeNode(node)
        addBefore(lfu.recent[node.frequency+1], node)
    } else if lfu.count[node.frequency] > 1 && lfu.recent[node.frequency] != node {
        // 不存在其他使用次数为n+1的缓存,但存在其他使用次数为n的缓存,且当前节点不是最近的节点
        // 将指定缓存移动到所有使用次数为n的节点之前
        removeNode(node)
        addBefore(lfu.recent[node.frequency], node)
    }
    // 更新recent
    lfu.recent[node.frequency+1] = node
    if lfu.count[node.frequency] <= 1 { // 不存在其他freq = n的节点,recent置空
        lfu.recent[node.frequency] = nil
    } else if lfu.recent[node.frequency] == node { // 存在其他freq = n的节点,且recent = node,将recent向后移动一位
        lfu.recent[node.frequency] = next
    }
    // 更新使用次数对应的节点数
    lfu.count[node.frequency+1]++
    lfu.count[node.frequency]--
    // 更新缓存使用次数
    node.frequency++
    return node.value
}

// Put 新建缓存
func (lfu *LFUCache) Put(key int, value int) {
    if lfu.capacity == 0 {
        return
    }
    node, ok := lfu.cache[key]
    if ok { // key已存在
        lfu.Get(key)
        node.value = value
        return
    }

    // key不存在
    if len(lfu.cache) >= lfu.capacity { // 缓存已满,删除最后一个节点,相应更新cache、count、recent(条件)
        tailNode := lfu.list.tail.prev
        lfu.list.removeTail()
        if lfu.count[tailNode.frequency] <= 1 {
            lfu.recent[tailNode.frequency] = nil
        }
        lfu.count[tailNode.frequency]--
        delete(lfu.cache, tailNode.key)
    }
    newNode := &doublyListNode{
        key:       key,
        value:     value,
        frequency: 1,
    }

    // 插入新的缓存节点
    if lfu.count[1] > 0 {
        addBefore(lfu.recent[1], newNode)
    } else {
        addBefore(lfu.list.tail, newNode)
    }

    // 更新recent、count、cache
    lfu.recent[1] = newNode
    lfu.count[1]++
    lfu.cache[key] = newNode
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容