描述
设计LRU缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能
-
set(key, value):将记录(key, value)插入该结构
-
get(key):返回key对应的value值
[要求]
-
set和get方法的时间复杂度为O(1)
-
某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的。
-
当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的。
若opt=1,接下来两个整数x, y,表示set(x, y)
若opt=2,接下来一个整数x,表示get(x),若x未出现过或已被移除,则返回-1
对于每个操作2,输出一个答案
示例1
输入:
[[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
返回值:
[1,-1]
说明:
第一次操作后:最常使用的记录为("1", 1)
第二次操作后:最常使用的记录为("2", 2),("1", 1)变为最不常用的
第三次操作后:最常使用的记录为("3", 2),("1", 1)还是最不常用的
第四次操作后:最常用的记录为("1", 1),("2", 2)变为最不常用的
第五次操作后:大小超过了3,所以移除此时最不常使用的记录("2", 2),加入记录("4", 4),并且为最常使用的记录,然后("3", 2)变为最不常使用的记录
解析:
LRU(Least Recently Used)最近最久未使用。
实现该算法一般有两种方式
-
数组方法:查询比较快,但是对于增删来说性能不是很好
-
链表方法:查询比较慢,但是对于增删来说十分方便,O(1)时间复杂度内搞定
首先来看一下数组方法:
/**
* lru design
* @param operators int整型二维数组 the ops
* @param k int整型 the k
* @return int整型一维数组
*/
package LRU_func2
// key和value分别放在一个数组里,两个数组的索引是对应的。
// 第一个元素是最早的元素,当数组的数量等于k时,入队前先移除第一个元素,再把新元素加进来。
func LRU2(operators [][]int, k int) []int {
result := make([]int, 0, len(operators))
key := make([]int, k) //存放key
value := make([]int, k) //存放value
for _, val := range operators {
if val[0] == 1 { //插入
//遍历key,查看是否存在该值,存在将该key放到数组第一个;不存在则判断数组大小,len==k时移除第一个并把该值加入数组。
var index = -1
for i, v := range key {
if v == val[1] { //找到了该key
key = append(key[1:], v)
value = append(value[1:], val[2])
index = i
break
}
}
if index == -1 { //没有找到key
if len(key) == k { //数组到上限了
//删除第一个,该值追加到最后一个
key = append(key[1:], val[1])
value = append(value[1:], val[2])
} else {
key = append(key, val[1])
value = append(value, val[2])
}
}
} else if val[0] == 2 { //获取
var index = -1
for i, v := range key {
if v == val[1] { //找到该值了
//把该值加入返回数组
result = append(result, value[i])
//把该值移动到最后一个
key = append(key[:i], append(key[i+1:], v)...)
value = append(value[:i], append(value[i+1:], value[i])...)
index = i
break
}
}
if index == -1 { //没有找到该值
//返回-1加入返回数组
result = append(result, -1)
}
}
}
return result
}
通过两个数组,一个存储key,一个存储value。
数组最前面的是最早的key,数组到达最大容量后,再往里存值就舍弃最前面的数值。
value数组的索引与key数组的索引是对应的。所以找到key对应的索引就能知道key对应的value值。
结论:可以看出来该算法每次查询会遍历key数组,复杂度与k有关。插入时,同样要遍历数组,复杂度与k相关。而且因为要把最新的key-value移动位置,每次都要移动数组元素。
在k相对较小的情况下使用该算法会比较有优势。
接下来看一下链表方法:
/**
* lru design
* @param operators int整型二维数组 the ops
* @param k int整型 the k
* @return int整型一维数组
*/
type Node struct {
next *Node
pre *Node
data int
}
var head = Node{new(Node), nil, 0}
var last = Node{nil, &head, 0}
var lruMap map[int]*Node
var result = make([]int, 0, 3)
var maxlen int
func LRU(operators [][]int, k int) []int {
//初始化head和last
head.next = &last
// write code here
//0 根据k生成一个k位的map
lruMap = make(map[int]*Node, k) //不能再声明容量。map特殊
maxlen = k
//1 遍历operators
for _, val := range operators {
switch val[0] {
case 1: //set
setLRUMap(val[1], val[2])
case 2: //get
getval := getLRUMap(val[1])
result = append(result, getval)
}
}
return result
}
func setLRUMap(k, v int) {
val, ok := lruMap[k]
if ok { //已经存在
val.data = v
//fmt.Println("V:", v)
val.next.pre = val.pre
val.pre.next = val.next
head.next.pre = val
val.next = head.next
val.pre = &head
head.next = val
} else { //不存在
newNode := Node{head.next, &head, v}
//判断是否超出限制了
if len(lruMap) >= maxlen {
//fmt.Println("remove last:", last.pre)
//移除map中的数据
delete(lruMap, last.pre.data)
//移除最后一个,新值添加到第一个
last.pre = last.pre.pre
last.pre.next = &last
head.next.pre = &newNode
head.next = &newNode
} else {
//fmt.Println("newNode", newNode)
head.next.pre = &newNode
head.next = &newNode
}
//lruMap中加入新值
lruMap[k] = &newNode
//fmt.Println("head.next:", head.next)
}
}
func getLRUMap(k int) int {
val, ok := lruMap[k]
if ok { //已经存在
val.next.pre = val.pre
val.pre.next = val.next
head.next.pre = val
val.next = head.next
val.pre = &head
head.next = val
return val.data
} else {
return -1
}
}
维护一个双向链表,用来进行入队出队操作。
通过一个map保存链表节点的地址。能匹配到key说明存在,则将对应的节点放到链表头。没有匹配的情况,先判断是否到达上限k了,到达后移除最后一个节点,将新节点添加到链表头部,没到上限直接加到链表头就行。
该方法对于查询及插入操作都是只有O(1)的复杂度,对于k较大的情况也很友好。
但是该方法目前尚存在问题,牛客用例过了百分之八十,错误用例因为太长不能获取到,还不知道问题出在哪。希望有大佬能帮忙指正。
两个方法都有些不完美,像数组方法,没有将get,set方法抽离出来;链表方法没有将链表操作抽离等。
加油吧!少年