开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情
LRU
LRU(Least Recently Used)是一种缓存淘汰算法,通常用于在缓存容量不足时淘汰最近最少使用的缓存数据,以保证缓存中的数据都是热点数据,提高缓存命中率。
缓存的作用就是存储最近使用的数据,最近用过了下一次也更可能再用到,但是容量有限。缓存的操作有:插入数据、查找数据
- 插入数据:把数据加入缓存,如果满了就删除一个,再插进去
- 查找数据:
- 命中:要查找的数据在缓存里,就命中了,可以直接返回数据
- Miss:没有命中,需要去磁盘读取数据,再把这个数据插入到缓存
LRU就是一种缓存淘汰策略,LRU就是当缓存满了,淘汰最近最久未使用的,就相当于对缓存的每个数据,记一下他们最后一次查询、插入的时间,最早来的就可以滚了,这里不欢迎没人要的人,新人和红人最受追捧。 因此要实现LRU就要记录使用的先后顺序,每次淘汰时,淘汰最久没人要的就好了
LRU接口
先定义一下接口的实现:
type LruCache interface {
New(cap int) *LruCache
Search(k int) (int, bool)
}
缓存其实存储的是键值对,用k来代表你需要的数据v,下面k和v都为int为例。
数组 + 时间戳
数组+时间戳的实现方式可以用一个固定大小的数组,每个元素保存一个键值对以及对应的时间戳。对于每个键值对,都记录一个时间戳,表示最近一次被访问的时间。当数组满时,如果需要添加新的键值对,则需要找到最旧的键值对,也就是访问时间最早的键值对,将其删除,再添加新的键值对。
type kv struct {
key string
value string
ts time.Time
}
type LRU struct {
size int
data []kv
current int
}
func NewLRU(size int) *LRU {
return &LRU{size: size, data: make([]kv, size)}
}
func (l *LRU) Get(key string) (string, bool) {
for i := range l.data {
if l.data[i].key == key {
l.data[i].ts = time.Now()
return l.data[i].value, true
}
}
return "", false
}
func (l *LRU) Set(key, value string) {
for i := range l.data {
if l.data[i].key == key {
l.data[i].value = value
l.data[i].ts = time.Now()
return
}
}
if l.current == l.size {
oldest := l.data[0].ts
oldestIdx := 0
for i := 1; i < l.size; i++ {
if l.data[i].ts.Before(oldest) {
oldest = l.data[i].ts
oldestIdx = i
}
}
l.data[oldestIdx].key = key
l.data[oldestIdx].value = value
l.data[oldestIdx].ts = time.Now()
} else {
l.data[l.current].key = key
l.data[l.current].value = value
l.data[l.current].ts = time.Now()
l.current++
}
}
这里空间复杂度为O(n)
get时间复杂度O(n),查询要枚举
set时间复杂度O(n),修改时也要枚举先找到
双向链表
数组一个带麻烦就是,删除一个节点,要把后面的依次向前挪动,这个太慢了,利用链表可以避免后面的挪动。
DoublyLinkedList 的 Add 方法用于向链表尾部添加一个节点。如果链表为空,就将头节点和尾节点都指向这个新节点。否则,将新节点的前向指针指向当前的尾节点,将当前尾节点的后向指针指向新节点,然后将尾节点指向新节点。
DoublyLinkedList 的 Remove 方法用于删除一个节点。删除一个节点时需要考虑两种情况:删除的是头节点和删除的是尾节点。如果删除的是头节点,就将头节点指向下一个节点;否则将要删除节点的前向指针指向要删除节点的后一个节点。
DoublyLinkedList 的 MoveToBack 方法用于将一个节点移动到链表的尾
type Node struct {
key int
value int
prev *Node
next *Node
}
type DoublyLinkedList struct {
head *Node
tail *Node
}
func NewDoublyLinkedList() *DoublyLinkedList {
return &DoublyLinkedList{nil, nil}
}
func (l *DoublyLinkedList) Add(node *Node) {
if l.head == nil {
l.head = node
l.tail = node
} else {
node.prev = l.tail
l.tail.next = node
l.tail = node
}
}
func (l *DoublyLinkedList) Remove(node *Node) {
if node.prev == nil {
l.head = node.next
} else {
node.prev.next = node.next
}
if node.next == nil {
l.tail = node.prev
} else {
node.next.prev = node.prev
}
}
func (l *DoublyLinkedList) MoveToBack(node *Node) {
l.Remove(node)
l.Add(node)
}
空间复杂度O(n)
插入时间复杂度O(1)
查询时间复杂度O(n),耗时在枚举链表的遍历
哈希+双向链表
那么对于双向链表的改进就是,加一个map记录k到链表节点的映射,这样子O(1)可以实现查询
import "container/list"
type LRUCache struct {
capacity int
size int
cache map[int]*list.Element
list *list.List
}
type cacheNode struct {
key int
value int
}
func Constructor(capacity int) LRUCache {
return LRUCache{
capacity: capacity,
cache: make(map[int]*list.Element),
list: list.New(),
}
}
func (this *LRUCache) Get(key int) int {
if elem, ok := this.cache[key]; ok {
this.list.MoveToFront(elem)
return elem.Value.(*cacheNode).value
}
return -1
}
func (this *LRUCache) Put(key int, value int) {
if elem, ok := this.cache[key]; ok {
elem.Value.(*cacheNode).value = value
this.list.MoveToFront(elem)
return
}
if this.size == this.capacity {
tail := this.list.Back()
delete(this.cache, tail.Value.(*cacheNode).key)
this.list.Remove(tail)
this.size--
}
elem := this.list.PushFront(&cacheNode{key, value})
this.cache[key] = elem
this.size++
}
空间复杂度O(n)
GetO(1)
PutO(1)
总的来说,哈希表 + 双向链表是最为常见和推荐的一种LRU实现方法,具有时间复杂度低、空间复杂度合理的优点。