介绍

LRU(Least Recently Used)和LFU(Least Frequently Used)是两种常见的缓存筛选算法, 用于在缓存空间有限的情况下挑选合适的缓存目标进行筛选,以提高缓存的利用功率。

LRU算法依据”最近最少运用”的准则进行筛选。它保护一个缓存的拜访次序链表,当有新的数据被拜访时,假如数据已经在缓存中,则将其移到链表头部;假如数据不在缓存中,则将其增加到链表头部。当需求筛选数据时,挑选链表尾部的数据进行筛选,因为尾部的数据是最近最少被拜访的数据。

LFU算法依据”最不常常运用”的准则进行筛选。它保护一个缓存目标的拜访频次,对于每个拜访到的目标,增加其拜访频次。当需求筛选数据时,挑选拜访频次最低的数据进行筛选。

LRU和LFU算法都有各自的优势和适用场景:

  • LRU算法适用于拜访具有时间局部性的数据,即最近被拜访的数据或许在将来一段时间内仍然会被拜访。LRU算法相对较简单,简单完成,适用于大多数场景。但是,当存在”热门数据”(被频繁拜访的数据)时,LRU算法或许无法有效地保证缓存的命中率。
  • LFU算法适用于拜访具有拜访频次局部性的数据,即拜访频次高的数据很或许在将来一段时间内仍然会被频繁拜访。LFU算法需求保护每个目标的拜访频次计数,相对于LRU算法来说更加复杂。LFU算法在面临热门数据的场景下表现较好,但在某些场景下或许存在”频次突变”的问题,即频次高的数据忽然不再被拜访,但因为频次计数较高而长期无法被筛选。

在实践运用中,我们能够依据具体的场景和需求挑选合适的缓存筛选战略,或者结合两种算法进行混合运用,以达到更好的缓存性能。

146. LRU 缓存 中等

下面用javascript代码演示一下LRU缓存:

// 最近最少运用
// 界说LRU缓存类
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.cache = new Map();
    }
    get (key) {
        if (this.cache.has(key)) {
            const value = this.cache.get(key);
            this.cache.delete(key); // 删去key在Map中的位置
            this.cache.set(key, value); // 重新设置key对在Map的最终(最近运用,也便是设置为最新的)
            return value;
        }
        return -1;
    }
    put (key, value) {
        if (this.cache.has(key)) {
            this.cache.delete(key); // 删去key在Map中的位置
        } else if (this.cache.size >= this.capacity) {
            // 缓存容量不够了
            const oldestKey = this.cache.keys().next().value;
            this.cache.delete(oldestKey); // 删去最旧的key
        }
        this.cache.set(key, value);
    }
}
// 示例用法
const cache = new LRUCache(2); // 创立容量为2的LRU缓存
cache.put(1, 1);
cache.put(2, 2);
console.log(cache.get(1)); // 输出 1
cache.put(3, 3);
console.log(cache.get(2)); // 输出 -1
console.log(cache.get(3)); // 输出 3
cache.put(4, 4);
console.log(cache.get(1)); // 输出 -1
console.log(cache.get(3)); // 输出 3
console.log(cache.get(4)); // 输出 4

这段代码的技巧就在于运用了ES6的Map和迭代器的功用。Map 的遍历次序便是刺进次序,运用Map.prototype.keys():回来键名的遍历器。然后我们运用迭代器的 next() 方法获取数据结构的第一个成员,也便是最旧的。然后通过 value 特点来获取它的值。拿到最旧的key就能够删去它,最终重新设置key。(Map具有LRU特性)

460. LFU 缓存 困难

javascript代码完成LFU:

// 最不常常运用
// 最不常常运用
class LFUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.cache = new Map();
        // 每个拜访次数都对应存储一个 Set,里边包括 cache 里边的 Map 的 key
        this.frequency = new Map();
        this.minFrequency = 0;
    }
    get(key) {
        if (this.cache.has(key)) {
            const { value, freq } = this.cache.get(key);
            this.updateFrequency(key, freq); // 更新拜访次数
            return value;
        }
        return -1;
    }
    put(key, value) {
        if (this.capacity === 0) return;
        if (this.cache.has(key)) {
            const { freq } = this.cache.get(key); // 获取当时元素旧的拜访次数
            this.cache.set(key, { value, freq }); // 更新当时 key 对应的元素
            this.updateFrequency(key, freq); // 更新当时的拜访次数
        } else {
            if (this.cache.size >= this.capacity) {
                // 获取拜访次数最少的 key 组成的 Set
                const leastFreq = this.frequency.get(this.minFrequency);
                // 依据 Set 的 LRU 特性,回来最久未被拜访的元素(假如拜访次数最少的元素有多个,最久未被拜访的元素会被删去)
                const deleteKey = leastFreq.keys().next().value;
                leastFreq.delete(deleteKey);
                this.cache.delete(deleteKey);
            }
            // 新增加元素时设置元素的拜访次数为 1
            this.cache.set(key,{value,freq: 1});
            if (!this.frequency.has(1)) {
                // 新增加元素时,假如不存在拜访次数为1对应的 Set,设置拜访次数为 1 的 Set 为空
                this.frequency.set(1,new Set())
            }
            // 设置拜访次数为1的 Set 中增加当时的key
            this.frequency.get(1).add(key);
            this.minFrequency = 1; // 新增加元素时最小拜访次数更新为 1
        }
    }
    updateFrequency(key, freq) {
        // 从当时拜访次数 freq 对应的 Set中,删去当时的 key
        const freqList = this.frequency.get(freq);
        freqList.delete(key);
        // 当时元素旧的拜访次数等于最小拜访次数而且当时拜访次数 freq 组成的 Set 为空
        //(元素旧的拜访次数等于最小拜访次数,而且最小拜访次数对应的 Set 为空的情况)
        // 假如某个元素它之前的拜访次数为最小拜访次数,而且没有其他元素与该元素的拜访次数相同
        if (freq === this.minFrequency && freqList.size === 0) {
            this.minFrequency++;  // 更新最小拜访次数
        }
        if (!this.frequency.has(freq + 1)) {
            this.frequency.set(freq + 1, new Set());
        }
        // 在更新后的拜访次数对应的 Set 里边增加当时元素对应的 key
        this.frequency.get(freq + 1).add(key);
        this.cache.get(key).freq = freq + 1; // 更新当时元素的拜访次数 + 1
    }
}
// 示例用法
const cache = new LFUCache(2); // 创立容量为2的LFU缓存
cache.put(1, 1);
cache.put(2, 2);
console.log(cache.get(1)); // 输出 1
cache.put(3, 3); // 删去 1
console.log(cache.get(2)); // 输出 2
console.log(cache.get(3)); // 输出 3
cache.put(4, 4); // 删去 2
console.log(cache.get(2)); // 输出 -1
console.log(cache.get(3)); // 输出 3
console.log(cache.get(4)); // 输出 4

相同这儿运用了 Set 数据结构的 LRU 特性,在 frequency 对应的 Map 里边放在着每种拜访次数对应的 key 组成的 Set。也便是说,拜访次数相同的元素所对应的 key 存放在同一个 Set 中。这儿要注意的便是新增元素时,设置拜访次数为1的 Set为空,而且增加拜访该元素的 key,一起设置最小拜访次数为1。假如某个元素它之前的拜访次数为最小拜访次数,而且没有其他元素与该元素的拜访次数相同,一起更新最小拜访次数和该元素的拜访次数,也便是该元素的拜访次数 + 1,而且设置对应的 Set 数据结构为空,把当时拜访的 key 增加进去。

go代码演示LRU缓存:

package main
import "fmt"
// LRUCache 最近最少运用
type LRUCache struct {
	size       int
	capacity   int
	cache      map[int]*DoubleLinkNode
	head, tail *DoubleLinkNode
}
// DoubleLinkNode 双向链表
type DoubleLinkNode struct {
	key, value int
	prev, next *DoubleLinkNode
}
// 初始化双向链表
func initDoubleLinkNode(key, value int) *DoubleLinkNode {
	return &DoubleLinkNode{
		key:   key,
		value: value,
	}
}
// Constructor 初始化LRU缓存
func Constructor(capacity int) LRUCache {
	l := LRUCache{
		capacity: capacity,
		cache:    map[int]*DoubleLinkNode{},
		head:     initDoubleLinkNode(0, 0),
		tail:     initDoubleLinkNode(0, 0),
	}
	l.head.next = l.tail
	l.tail.prev = l.head
	return l
}
func (l *LRUCache) Get(key int) int {
	if _, ok := l.cache[key]; !ok {
		return -1
	}
	node := l.cache[key]
	l.moveToHead(node)
	return node.value
}
func (l *LRUCache) Put(key, value int) {
	// 新增元素 放在链表头部
	if _, ok := l.cache[key]; !ok {
		node := initDoubleLinkNode(key, value)
		l.cache[key] = node
		l.addToHead(node)
		l.size++
		// 超出容量 删去最终一个元素
		if l.size > l.capacity {
			removed := l.removeTail()
			delete(l.cache, removed.key)
			l.size--
		}
	} else {
		// 更新元素 放在链表头部
		node := l.cache[key]
		node.value = value
		l.moveToHead(node)
	}
}
func (l *LRUCache) addToHead(node *DoubleLinkNode) {
	node.prev = l.head
	node.next = l.head.next
	l.head.next.prev = node
	l.head.next = node
}
func (l *LRUCache) removeNode(node *DoubleLinkNode) {
	node.prev.next = node.next
	node.next.prev = node.prev
}
func (l *LRUCache) moveToHead(node *DoubleLinkNode) {
	l.removeNode(node)
	l.addToHead(node)
}
func (l *LRUCache) removeTail() *DoubleLinkNode {
	node := l.tail.prev
	l.removeNode(node)
	return node
}
func main() {
	cache := Constructor(2)
	cache.Put(1, 1)
	cache.Put(2, 2)
	fmt.Println(cache.Get(1)) //删去2 输出 1
	cache.Put(3, 3)
	fmt.Println(cache.Get(2)) // 输出 -1
	cache.Put(4, 4) // 删去 1
	fmt.Println(cache.Get(1)) // 输出 -1
	fmt.Println(cache.Get(3)) // 输出 3
	fmt.Println(cache.Get(4)) // 输出 4
}

代码运用了双向链表 + map完成了LRU数据结构。代码中采用了首尾虚拟节点,拜访到的节点移动到头部,超出容量时删去尾部的节点。

go代码完成LFU数据结构:

package main
import "fmt"
// LFUCache 最不常常运用
type LFUCache struct {
	size     int // 大小
	capacity int // 容量
	minFreq  int // 最小拜访次数
	cache    map[int]*Node
	freq     map[int]*DLinkedList
}
type Node struct {
	key        int
	value      int
	freq       int // 拜访次数
	prev, next *Node
}
// DLinkedList 拜访次数为n的元素组成的双向链表(LRU特性)
type DLinkedList struct {
	head, tail *Node
}
func Constructor(capacity int) LFUCache {
	return LFUCache{
		capacity: capacity,
		cache:    make(map[int]*Node),
		freq:     make(map[int]*DLinkedList),
	}
}
func InitDLinkedList() *DLinkedList {
	head, tail := &Node{}, &Node{}
	head.next = tail
	tail.prev = head
	return &DLinkedList{
		head: head,
		tail: tail,
	}
}
func (d *DLinkedList) AddToHead(node *Node) {
	node.prev = d.head
	node.next = d.head.next
	d.head.next.prev = node
	d.head.next = node
}
func (d *DLinkedList) Remove(node *Node) {
	node.prev.next = node.next
	node.next.prev = node.prev
	node.prev = nil
	node.next = nil
}
func (d *DLinkedList) RemoveTail() *Node {
	// 空链表
	if d.IsEmpty() {
		return nil
	}
	last := d.tail.prev
	d.Remove(last)
	return last
}
func (d *DLinkedList) IsEmpty() bool {
	return d.head.next == d.tail
}
func (l *LFUCache) Get(key int) int {
	if node, ok := l.cache[key]; ok {
		l.addFreq(node)
		return node.value
	}
	return -1
}
func (l *LFUCache) Put(key, value int) {
	if l.capacity == 0 {
		return
	}
	// 更新
	if node, ok := l.cache[key]; ok {
		node.value = value
		l.addFreq(node)
	} else {
		// 超出容量,删去拜访次数最低的对应数据链表的最旧的元素
		if l.size >= l.capacity {
			node := l.freq[l.minFreq].RemoveTail()
			delete(l.cache, node.key)
			l.size--
		}
		node := &Node{key: key, value: value, freq: 1}
		l.cache[key] = node // 贮存新节点
		if l.freq[1] == nil {
			l.freq[1] = InitDLinkedList()
		}
		l.freq[1].AddToHead(node)
		l.minFreq = 1
		l.size++
	}
}
func (l *LFUCache) addFreq(node *Node) {
	n := node.freq
	l.freq[n].Remove(node)
	if l.minFreq == n && l.freq[n].IsEmpty() {
		l.minFreq++
		delete(l.freq, n)
	}
	node.freq++
	if l.freq[node.freq] == nil {
		l.freq[node.freq] = InitDLinkedList()
	}
	l.freq[node.freq].AddToHead(node)
}
func main() {
	cache := Constructor(2)
	cache.Put(1, 1)
	cache.Put(2, 2)
	fmt.Println(cache.Get(1)) // 1
	cache.Put(3, 3)           // 删去2
	fmt.Println(cache.Get(2)) // -1
	fmt.Println(cache.Get(3)) // 3
	cache.Put(4, 4) // key2被筛选
	fmt.Println(cache.Get(1)) // -1
	fmt.Println(cache.Get(3)) // 3
	fmt.Println(cache.Get(4)) // 4
}

cache存储对应的数据节点,但是或许会出现节点拜访次数相同的情况。对应拜访次数相同的节点,我们把放在一个双向链表中,然后依据LRU最近最少运用,超越容量的时候删去双向链表尾部的节点。拿到节点的key,之后就能从map中删去贮存的节点了。