LRU淘汰算法
LRU,全称为Least Recently Used,意思为最近最少使用,是一种内存淘汰算法,常用在缓存中作为一种淘汰策略。
计算机内存是有有限的,因此不可能无限的增加数据,缓存也是内存的一种,存在于内存中,缓存容量是有限制的,当缓存满了,就需要删除一些数据,给新的数据腾空间。
但这里有个问题是,应该删除哪些缓存呢?对于我们来说,肯定希望将没用的缓存删掉,留下有用的,但是怎么判定缓存是没用的?
常用的缓存淘汰算法有:FIFO(先进先出),LFU(Least Frequently Used 最少频率使用),LRU
接下来就来学习下LRU算法的实现,他要淘汰的数据是:最近最少使用的。
LRU算法解析
可以先用大白话来描述下LRU的实现步骤:
可以暂时先想象将所有缓存放在一个队列上,队列是按时间排列的,队列头为最近一次使用过的。
- 设置缓存,判断缓存是否已经存在了,存在则需要完成:
- 将缓存放在最前面(表示最近使用了)
- 更新缓存值
- 如果缓存不存在,则要新增缓存到最前面,并且判断缓存容量是否已满
- 如果缓存容量满了,则要将最后面的一个缓存删除
- 获取缓存,缓存存在,则获取,并且移动缓存到最前面
根据上面大致分析,其实LRU的核心就是一个数据结构,还需要一个容量,表示缓存容量,另外还需要提供两个接口:get(key)
和put(key, value)
,分别为获取缓存和设置缓存。
那我们接下来就要考虑使用哪些数据结构来实现了,从LRU的
根据算法的时间复杂度,get和put方法最理想的时间复杂度是O(1),而且LRU的操作里有查找,删除,插入,并且是有顺序之分
那有什么数据结构能满足以上条件的?
- 哈希表:哈希表查询快,但是数据是无序的
- 链表:链表是可以有序的,插入删除都快,但是查找很慢(要遍历整条链)
- 双向链表:满足插入删除快,并且操作头和尾很方便
所以要同时满足上述条件,我们可以考虑使用哈希表+双向链表
来实现LRU,这两个数据结构的结合满足了条件。
LRU算法实现(Golang)
双向链表直接使用了Golang标准库的container/list
,哈希表使用了map
package lru
import (
"container/list"
"fmt"
)
type Cache struct {
capacity int // 缓存容量
list *list.List // 双向链表
hashmap map[string]*list.Element // 哈希表
}
// 链表节点保存的数据,需要带上key
type CacheValue struct {
key string
value interface{}
}
func NewCache(capactiy int) *Cache {
return &Cache{
capacity: capactiy,
list: new(list.List),
hashmap: make(map[string]*list.Element),
}
}
// 获取缓存
func (c *Cache) Get(key string) (interface{}, bool) {
// 判断缓存是否已存在
if v, ok := c.hashmap[key]; ok { // 存在
// 将key移到链表头
c.list.MoveToFront(v)
// 返回
cacheValue := v.Value.(*CacheValue)
return cacheValue.value, true
}
return nil, false
}
// 设置缓存
func (c *Cache) Put(key string, value interface{}) {
// 缓存存在
if v, ok := c.hashmap[key]; ok {
// 将缓存移动到链表头
c.list.MoveToFront(v)
// 更新数据
cacheValue := v.Value.(*CacheValue)
cacheValue.value = value
} else {
// 缓存不存在,直接在链表头插入一个缓存
e := c.list.PushFront(&CacheValue{key, value})
// 插入哈希表
c.hashmap[key] = e
// 判断是否超过最大容量
if len(c.hashmap) > c.capacity && c.capacity != 0 {
// 获取链表尾巴的缓存
back := c.list.Back()
if back != nil {
key := back.Value.(*CacheValue).key
// 移除哈希表中的key
delete(c.hashmap, key)
// 移除链表尾巴的元素
c.list.Remove(back)
}
}
}
}
// 测试使用
func (c *Cache) Capacity() int {
return c.capacity
}
func (c *Cache) PrintList() {
ele := c.list.Front()
for i := 0; i < c.list.Len(); i++ {
kv := ele.Value.(*CacheValue)
fmt.Printf("%v = %v ", kv.key, kv.value)
ele = ele.Next()
}
fmt.Println()
}
测试:
package main
import (
"alg/lru"
)
func main() {
cache := lru.NewCache(5)
cache.Put("key1", 1)
cache.Put("key2", 2)
cache.Put("key3", 3)
cache.Put("key4", 4)
cache.Put("key5", 5)
// 测试1:获取key,验证是否会提取到链表头
// value, ok := cache.Get("key2")
// if !ok {
// fmt.Println("get key2 fail")
// } else {
// fmt.Println("key2 = ", value)
// }
// cache.PrintList()
// 测试2:缓存满了之后,新增是否会移除最少使用那个
// cache.PrintList()
// cache.Put("key6", 6)
// cache.PrintList()
// 测试3:缓存满了之后,更新已存在的值,看看是否会更新值并且移动到队列头
cache.PrintList()
cache.Put("key3", "key3")
cache.PrintList()
}