最近想锻炼一下笔试能力。翻开牛客题目,做一下LRU。
题目要求空间复杂度为O(1),我的代码超时了。待优化,贴下来。等我搞懂了再写详细点
··········第一次尝试 ROUND 1··········
### 题目要求
设计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,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
输出:[1,-1]
其实我审题没明白,看了高赞回答,才懂
二维数组中第0位即:[1,1,1],第一个1表示opt=1,要set(1,1),即要将(1,1)插入缓存
二维数组中第1为即:[1,2,2],第一个1表示opt=1,要set(2,2),即要将(2,2)插入缓存
二维数组中第2位即:[1,3,2],第一个1表示opt=1,要set(3,2),即要将(3,2)插入缓存
二维数组中第3位即:[2,1],第一个2表示opt=2,要get(1),即从缓存中查找key为1的值,前面已经插入了key=1,所以返回1,这个要保存到返回数组中
二维数组中第5位即:[2,2],第一个2表示opt=2,要get(2),即从缓存中查找key为2的值,由于缓存大小为3,所以前面插入的key=2已经被挤出缓存,所以返回结果为-1,这个要保存到返回数组中
所以输出为[1,-1]
示例1中,输入最后一个单独的3表示缓存大小
我的代码
class Solution:
def LRU(operators , k ):
# write code here
class LRU_cache:
def __init__(self,k):
self.lenth=k
self.present=0
self.cache=dict()
self.quque=[]
self.output=[]
def _set(self,l):
#修改quque和cache
key=l[0]
val=l[1]
self.cache[key]=val
if key in self.quque:
self.quque.remove(key)
self.quque=[key]+self.quque
if len(self.quque)>self.lenth:
self.cache.pop(self.quque[-1])
self.quque=self.quque[:-1]
print('set一次',self.cache,self.quque)
def _get(self,key):
if key in self.cache.keys():
self.output.append(self.cache.get(key))
self.quque.remove(key)
self.quque=[key]+self.quque
else:
self.output.append(-1)
print('get一次',self.cache,self.quque)
cache=LRU_cache(k)
for l in operators:
if l[0]==1:
cache._set(l[1:])
else:
cache._get(l[1])
print('操作一次',cache.cache,cache.quque)
return(cache.output)
··········第二次尝试 ROUND 2··········
第一版的算法是用一个list模拟了这个cache,在增、查操作的时候都需要遍历这个列表,时间复杂度为O(n),不是常数级。
看了答案解析,要实现O(1)的时间开销,需要使用哈希表数据结构。在python中,字典dict
便是哈希结构。在两个操作中对数据进行读写时,通过哈希索引快速定位。在移动键值数据时,链表结构才能满足O(1)的时间需求,只需要断掉当前链接,再在指定位置链接进去即可。
答案解析给出的方式是哈希表加双向链表的结构。
参考代码如下
标准答案
class DLinkedNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cache = dict()
# 使用伪头部和伪尾部节点
self.head = DLinkedNode()
self.tail = DLinkedNode()
self.head.next = self.tail
self.tail.prev = self.head
self.capacity = capacity
self.size = 0
def get(self, key: int) -> int:
if key not in self.cache:
return -1
# 如果 key 存在,先通过哈希表定位,再移到头部
node = self.cache[key]
self.moveToHead(node)
return node.value
def put(self, key: int, value: int) -> None:
if key not in self.cache:
# 如果 key 不存在,创建一个新的节点
node = DLinkedNode(key, value)
# 添加进哈希表
self.cache[key] = node
# 添加至双向链表的头部
self.addToHead(node)
self.size += 1
if self.size > self.capacity:
# 如果超出容量,删除双向链表的尾部节点
removed = self.removeTail()
# 删除哈希表中对应的项
self.cache.pop(removed.key)
self.size -= 1
else:
# 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node = self.cache[key]
node.value = value
self.moveToHead(node)
def addToHead(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def removeNode(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def moveToHead(self, node):
self.removeNode(node)
self.addToHead(node)
def removeTail(self):
node = self.tail.prev
self.removeNode(node)
return node
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/lru-cache/solution/lruhuan-cun-ji-zhi-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
以及一个利用python原生数据结构collections.OrderedDict
取巧的答案
class LRUCache(collections.OrderedDict):
def __init__(self, capacity: int):
super().__init__()
self.capacity = capacity
def get(self, key: int) -> int:
if key not in self:
return -1
self.move_to_end(key)
return self[key]
def put(self, key: int, value: int) -> None:
if key in self:
self.move_to_end(key)
self[key] = value
if len(self) > self.capacity:
self.popitem(last=False)
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/lru-cache/solution/lruhuan-cun-ji-zhi-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
那么在参考了解题思路以后,我又编写了一次程序,但是此时的我并没有理解为什么要将双向链表头尾相连成循环链表,觉得循环链表在尾部丢弃操作会带来意想不到的问题。于是本人第二次代码如下:
class Solution:
def LRU(operators , k ):
# write code here
class DataNode:
def __init__(self,l:list):
self.key=l[0]
self.value=l[1]
self.pri=None
self.beh=None
class LRU_cache:
def __init__(self,k):
self.length=k
self.present=None
self.last=None
self.head=None
self.cache=dict()
def _set(self,l:list):
key=l[0]
value=l[1]
if len(self.cache)==0:
node=DataNode(l)
self.cache[key]=node
self.head=node
self.last=node
elif key in self.cache.keys():
node=self.cache[key]
node.value=value
if self.last.key==key and self.last.key!=self.head.key:
self.last=node.beh
self.last.pri=None
node.pri=self.head
self.head.beh=node
self.head=node
node.beh=None
elif self.head.key==key and self.last.key!=self.head.key:
pass
elif len(self.cache)==1:
pass
else:
node.pri.beh=node.beh
node.beh.pri=node.pre
node.pri=self.head
self.head=node
node.beh=None
elif key not in self.cache.keys() and len(self.cache)==self.length:
node=DataNode(l)
#丢出最后一个节点
self.cache.pop(self.last.key)
self.last=self.last.beh
self.last.pri=None
#插进新的节点
self.cache[key]=node
node.pri=self.head
self.head.beh=node
self.head=node
self.head.beh=None
else:
#变头不变尾
node=DataNode(l)
self.cache[key]=node
node.pri=self.head
self.head.beh=node
self.head=node
self.head.beh=None
def _get(self,key) ->int:
if key in self.cache.keys():
#该节点调到开头
node=self.cache[key]
if self.last.key==key and self.last.key!=self.head.key:
self.last=node.beh
self.last.pri=None
node.pri=self.head
self.head.beh=node
self.head=node
node.beh=None
elif self.head.key==key and self.last.key!=self.head.key:
pass
elif len(self.cache)==1:
pass
else:
node.pri.beh=node.beh
node.beh.pri=node.pre
node.pri=self.head
self.head=node
node.beh=None
return(node.value)
else:
return(-1)
output=[]
cache=LRU_cache(k)
for l in operators:
if l[0]==1:
cache._set(l[1:])
else:
output.append(cache._get(l[1]))
return(output)
a=Solution
print(a.LRU([[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3))
在编写过程中,我就意识到为什么要使用循环链表:
在插入和读取操作中,每当涉及到节点激活成最新使用节点时,不循环链表都要考虑
- 被移动的节点在链表前端
- 被移动的节点在链表后端
- 被移动的节点在链表中间
这三种情况,相当痛苦。if...else...
已经把自己绕晕了。而且我脑内模拟了一下,循环链表不会在删除最后一个节点时带来麻烦。
那第二次提交的结果如何呢?
答:例子正确输出了,但在测试其他输入时候报了节点没有前一节点的错,不用说,肯定是前面所说的不循环链表端点处理除了问题。
我也有准备debug一下,但是牛客IDE里没发考出完整的输入,加上我上述代码疯狂的if-else已经成为了传说中的‘屎山’,看的我头疼,第二次挑战就此作罢。准备第三次挑战,这次要改进的地方是链表改成循环链表。