缓存世界的智囊团:揭秘LRU算法的黑科技实现!
能Get到什么知识点
- lru算法是什么
- 怎么实现
引言
最早接触lru算法的时候是学习redis的时候,学到reids的key的淘汰策略,那时候还不知道该算法大概是怎样的。接下来我的操作。。。
1. 什么是Lru算法
对应leetcode的leetcode.cn/problems/lr…
来自于百度百科
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法选择最近最久未使用的页面予以淘汰。该算法赋予每个一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
简单的来说就是,用到谁谁就在第一个,满了删除最后一个。
2. 使用场景
- 最近使用的表情包。
- 一些web页面的缓存
- 数据库查询结果缓存
3. 具体实现
3.1 分为两个操作
- Put
- Get
当Put
时也就是插入或更新一条新的值时,需要进行判断是否已经存在了。如果存在就进行更新该值,就可以说是最近使用了的,依据lru算法最近使用了的应该是最新的,放在头部或说明是个最新的数据。如果该值不存在需要进行插入操作,但是缓存嘛!缓冲太多也不合适。一般的做法是给lru
限定一个容量(capacity),如果超过了容量就进行删除最久远未使用过的,然后在进行插入操作。
当Get
操作时,进行获取操作,更新为最新使用的。
3.2 选择什么数据结构呢?
- 哈希表+双向链表
为什么选择哈希表+双向链表?
哈希表可以用来存储该key,对应的value应该是节点对象,由于节点对象是引用类型,可以快速获取和修改链表进行更新操作。
3.3 Coding
接下是Go语言版本的
定义一个LRUNode
go
复制代码
type LRUNode struct {
Key int // 存储key,用于之后进行在hs表进行删除该key
Val int
Pre *LRUNode
Next *LRUNode
}
func NewLRUNode(key, val int) *LRUNode {
return &LRUNode{Val: val, Key: key}
}
go
复制代码
type LRUCache struct {
//记录node节点的缓存
Mp map[int]*LRUNode
Head *LRUNode
//wei节点
Tail *LRUNode
//最大容量
MaxSize int
//当前容量 ,实现获取容量是达到O(1)操作
CrtSize int
}
func Constructor(capacity int) *LRUCache {
//进行构造应该LRU双向链表
head := NewLRUNode(0, 0)
tail := NewLRUNode(0, 0)
head.Next = tail
tail.Pre = head
return &LRUCache{MaxSize: capacity, Mp: make(map[int]*LRUNode, capacity), Head: head, Tail: tail}
}
func (l *LRUCache) Put(key int, value int) {
// 1.插入
node, ok := l.Mp[key]
//存在更新,放入链表头
if ok {
//1. 更新值
node.Val = value
//删除该节点位置
l.RemoveNode(node)
//移动头
l.MoveTop(node)
} else {
//1. 不存在创建
node := NewLRUNode(key, value)
//2. 放入链头
l.MoveTop(node)
//3. 加入到Mp
l.Mp[key] = node
l.CrtSize += 1
//4.是否当前节点数大于了最大的,超过了去除最后一个节点
if l.CrtSize > l.MaxSize {
// 清理最后一个
clear := l.Tail.Pre
l.RemoveNode(clear)
delete(l.Mp, clear.Key)
}
}
}
func (l *LRUCache) MoveTop(node *LRUNode) {
//1. 获取之前的头
first := l.Head.Next
node.Pre = l.Head
l.Head.Next = node
node.Next = first
first.Pre = node
}
func (l *LRUCache) RemoveNode(node *LRUNode) {
next := node.Next
pre := node.Pre
pre.Next = next
next.Pre = pre
}
func (l *LRUCache) Get(key int) int {
// 获取对象
node, ok := l.Mp[key]
//不存在
if !ok {
return -1
}
// 存在,更新到链头,返回值
l.RemoveNode(node)
l.MoveTop(node)
return node.Val
}
func main() {
lRUCache := Constructor(2)
lRUCache.Put(1, 1) // 缓存是 {1=1}
lRUCache.Put(2, 2) // 缓存是 {1=1, 2=2}
fmt.Println(lRUCache.Get(1)) // 返回 1
lRUCache.Put(3, 3) // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
fmt.Println(lRUCache.Get(2)) // 返回 -1 (未找到)
lRUCache.Put(4, 4) // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
fmt.Println(lRUCache.Get(1)) // 返回 -1 (未找到)
fmt.Println(lRUCache.Get(3)) // 返回 3
fmt.Println(lRUCache.Get(4))
fmt.Println(lRUCache.Mp)
}
大功告成!!!
总结
- 淘汰最久未被访问数据: 当缓存空间已满且需要淘汰数据时,LRU算法会选择淘汰最久未被访问的数据项。
- 保留最近被访问数据: LRU算法会尽量保留最近被访问的数据。
- 适用于频繁访问的数据集: LRU算法特别适用于访问模式相对稳定的数据集,即某些数据项被频繁访问,而其他数据项则很少或不被访问。
- 时间复杂度: 可以使用更高效的数据结构(如哈希表 + 双向链表)来实现LRU算法,从而降低时间复杂度。
- 局限性: LRU算法可能在某些场景下表现不佳,例如在数据访问模式不稳定、缓存空间较小时,或者数据集较大时,可能导致缓存命中率下降。例如:web网页,表情包最近使用。