第六届字节跳动青训营第四课(Lru算法实践) | 青训营

32 阅读4分钟

缓存世界的智囊团:揭秘LRU算法的黑科技实现!

能Get到什么知识点

  • lru算法是什么
  • 怎么实现

引言

最早接触lru算法的时候是学习redis的时候,学到reidskey的淘汰策略,那时候还不知道该算法大概是怎样的。接下来我的操作。。。

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)  
  
}

大功告成!!!

总结

  1. 淘汰最久未被访问数据: 当缓存空间已满且需要淘汰数据时,LRU算法会选择淘汰最久未被访问的数据项。
  2. 保留最近被访问数据: LRU算法会尽量保留最近被访问的数据。
  3. 适用于频繁访问的数据集: LRU算法特别适用于访问模式相对稳定的数据集,即某些数据项被频繁访问,而其他数据项则很少或不被访问。
  4. 时间复杂度: 可以使用更高效的数据结构(如哈希表 + 双向链表)来实现LRU算法,从而降低时间复杂度。
  5. 局限性: LRU算法可能在某些场景下表现不佳,例如在数据访问模式不稳定、缓存空间较小时,或者数据集较大时,可能导致缓存命中率下降。例如:web网页,表情包最近使用。