缓存淘汰策略

LRU

LRU(Least Recently Used) 最近最久未使用算法,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。

比方说手机杀后台,同时开启了若干个应用在后台,再开一个内存就不够了,这时系统就会把后台中最久没有使用过的程序杀掉。

一个 LRU 算法通常要实现以下的功能:

  1. 接受一个 max 参数作为缓存的最大容量。
  2. 有一个 put(key, val) 方法用来存键值对。
  3. 有一个 get(key) 用来取对应的值,找不到返回 -1。同时,触发该方法也会改变顺序。

我们可以利用数组实现算法:

function LRU(max) {
  this.cache = []
  this.max = max
}
LRU.prototype.find = function(key) {
  return this.cache.findIndex((item) => item.key === key)
}
LRU.prototype.put = function(key, val) {
  const index = this.find(key)
  if (index > -1) {
    // 如果相同key的数据已经存在,就将其移动到数组头部
    this.cache.splice(index, 1)
  } else if (this.cache.length >= this.max) {
    // 达到或超出最大缓存限制,就删除最后的
    this.cache.pop()
  }
  this.cache.unshift({ key, val })
}
LRU.prototype.get = function(key) {
  const index = this.find(key)
  if (index > -1) {
    const cache = this.cache
    const temp = cache[index].val
    cache.splice(index, 1)
    cache.unshift({ key, val: temp })
    return temp
    return
  } else {
    return -1
  }
}

由于数组需要进行遍历检查 key 值,因此我们可以使用 Map 来存储以达到时间复杂度 $O(1)$:

function LRU(max) {
  this.cache = new Map()
  this.max = max
}
LRU.prototype.has = function(key) {
  return this.cache.has(key)
}
LRU.prototype.put = function(key, val) {
  const cache = this.cache
  if (this.has(key)) {
    // 如果相同key的数据已经存在,就将其移动到数组头部
    cache.delete(key)
  } else if (cache.size >= this.max) {
    // 达到或超出最大缓存限制,就删除最后的
    cache.delete(cache.keys().next().value)
  }
  cache.set(key, val)
}
LRU.prototype.get = function(key) {
  if (this.has(key)) {
    const cache = this.cache
    const temp = cache.get(key)
    cache.delete(key)
    cache.set(key, temp)
    return temp
  } else {
    return -1
  }
}

LFU

LFU(Least Frequently Used) 最少使用算法,也就是使用频率最少的数据。

和 LRU 不同,LRU 按照访问先后排序,而 LFU 则是按照访问次数排序,因此每次添加或获取数据的时候,我们需要根据频率更新列表的排序。

我们可以使用双向链表+哈希表来实现 LFU。

class Node {
  constructor(key, val) {
    this.key = key
    this.val = val
    this.freq = 0 // 频率
    this.pre = null // 上一个节点
    this.next = null // 下一个节点
  }
}
// 双向链表,接受一个初始节点做初始化
class DoubleLinkedList {
  constructor() {
    // 我们需要先创造两个哑巴节点,这是为了省去对边界节点的额外处理,使得每个节点的处理过程一样
    this.head = new Node('head') // 头指针
    this.tail = new Node('tail') // 尾指针
    this.head.next = this.tail
    this.tail.pre = this.head
  }
  remove(node) {
    node.pre.next = node.next
    node.next.pre = node.pre
  }
  // 在preNode后面插入节点
  insert(preNode, node) {
    const nextNode = preNode.next
    nextNode.pre = node
    node.next = nextNode
    preNode.next = node
    node.pre = preNode
  }
}
class LFU {
  constructor(max = 0) {
    this.max = max <= 0 ? 0 : max
    this.size = 0
    this.cache = new Map()
    this.list = null
  }
  // 增加节点的频率并更新链表顺序
  increaseFreq(node) {
    const cpyNode = node
    cpyNode.freq++
    const curFreq = cpyNode.freq
    // 从当前节点开始遍历直到freq小于前面的节点为止
    node = node.pre || this.list.tail.pre
    while (node.freq <= curFreq) {
      // 遍历到头节点跳出
      if (node === this.list.head) {
        break
      }
      node = node.pre
    }
    this.list.insert(node, cpyNode)
    this.size++
  }
  // 新增节点
  put(key, val) {
    if (this.max === 0) return void 0
    const cache = this.cache
    if (cache.has(key)) {
      // 如果已存在,就只更新val和增加频率
      let node = cache.get(key)
      node.val = val
      this.list.remove(node)
      this.increaseFreq(node)
    } else {
      /**
       * 判断是否达到最大缓存
       * 是:移除频率最小(链表尾指针指向)的node,并将新节点插入
       * 否:将新的节点并插入到链表尾部
       */
      if (this.size >= this.max) {
        const list = this.list
        const removedNode = list.tail.pre
        list.remove(removedNode)
        this.cache.delete(removedNode.key)
        this.size--
      }
      const node = new Node(key, val)
      this.cache.set(key, node)
      if (this.list === null) {
        this.list = new DoubleLinkedList()
      }
      const list = this.list
      this.increaseFreq(node)
    }
  }
  // 获取节点
  get(key) {
    const cache = this.cache
    if (cache.has(key)) {
      const node = cache.get(key)
      this.increaseFreq(node)
      return node
    } else {
      return -1
    }
  }
  log() {
    let node = this.list.head
    let res = ''
    let sum = 0
    while (true) {
      res += `(${node.key}, ${node.val}, ${node.freq})`
      if (node.next === null) {
        break
      }
      res += '-->'
      node = node.next
    }
    console.log(res)
  }
}
const lfu = new LFU(4)
lfu.put(1, '第一个节点')
lfu.put(2, '第二个节点')
lfu.put(3, '第三个节点')
lfu.put(3, '第三个节点+')
lfu.put(3, '第三个节点++')
lfu.put(4, '第四个节点')
lfu.log()
// (head, undefined, 0)-->(3, 第三个节点++, 3)-->(4, 第四个节点, 1)-->(2, 第二个节点, 1)-->(tail, undefined, 0)

也可以直接利用 Map 的特性:被 set 的元素会移到尾部。

class Node {
  constructor(key, value) {
    this.key = key
    this.value = value
    this.freq = 1 // 访问频次
  }
}

class LFUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.minFreq = 0 // 最小使用频率,删除使用
    this.size = 0 // 当前已使用的容量
    this.freqMap = new Map() // 频率-map<node>(不使用双向链表)
    this.cacheMap = new Map() // key-node(用于get方法,快捷获取相对应key的value值)
  }

  /**
   * 频率+1
   * ① 从freqMap的旧频率的key中移除
   * ② 旧频率为空 && 当前频率是最小频率,需要将最小频率+1
   * ③ 添加到新频率(旧频率+1)对应的map队列中 => 当前队列没有需要新建
   */
  increaseFreq(node) {
    let map = this.freqMap.get(node.freq)
    map.delete(node.key)
    if (!map.size && node.freq === this.minFreq) {
      this.minFreq += 1
    }
    node.freq += 1
    let newMap = this.freqMap.get(node.freq)
    if (newMap === undefined) {
      newMap = new Map()
    }
    newMap.set(node.key, node)
    this.freqMap.set(node.freq, newMap)
  }

  // 借助缓存的 cacheMap 处理
  get(key) {
    let node = this.cacheMap.get(key)
    if (node === undefined) {
      return -1
    }
    this.increaseFreq(node)
    return node.value
  }

  put(key, value) {
    if (this.capacity === 0) return void 0
    const cacheMap = this.cacheMap
    const freqMap = this.freqMap
    // 如果节点已存在,则变更其值
    if (cacheMap.has(key)) {
      let node = cacheMap.get(key)
      node.value = value
      cacheMap.set(key, node)
      this.increaseFreq(node)
      return void 0
    }
    // 达到最大容量,需要删除访问频次最小&&最久的节点
    // 访问频次最小=>通过minFreq标记获取相应的map
    // map中第一个元素为最久未被访问元素(map lru特性决定)
    if (this.size === this.capacity) {
      let miniFreqMap = freqMap.get(this.minFreq)
      let miniFreqNode = miniFreqMap.get(miniFreqMap.keys().next().value) // 频次最小,第一个key(借助map的lru特性)
      miniFreqMap.delete(miniFreqNode.key)
      freqMap.set(this.minFreq, miniFreqMap)
      cacheMap.delete(miniFreqNode.key)
    }
    // 增加节点元素(这里只处理节点不存在,已存在上面已处理)
    let node = new Node(key, value)
    cacheMap.set(key, node)
    let map = freqMap.get(1)
    if (map === undefined) {
      map = new Map()
    }
    map.set(node.key, node)
    // 对应新节点,访问频次一定是1,且最小频次为1
    freqMap.set(1, map)
    this.minFreq = 1
    if (this.size < this.capacity) {
      this.size += 1
    }
    return void 0
  }
}

参考文章