牛客刷题--NC93 设计LRU缓存结构

2 篇文章 0 订阅

描述

设计LRU缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能

  • set(key, value):将记录(key, value)插入该结构

  • get(key):返回key对应的value值

[要求]

  1. set和get方法的时间复杂度为O(1)

  2. 某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的。

  3. 当缓存的大小超过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方法抽离出来;链表方法没有将链表操作抽离​等。

加油吧​!少年​

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值