LRUCache全名为Least Recently Used,即最近最少使用算法,是操作系统中发生缺页中断时常用的一种页面置换算法。
根据局部性原理,最近使用的数据块很有可能继续被频繁使用,因此当Cache已满的时候,LRUCache算法会把最久未使用的数据块替换出去。
对于LRU算法主要实现两个操作:
- 访问数据块
- 将访问的数据块更新为最近访问,并返回访问的数据块。
- 添加数据块
- 如果Cache还有容量,将添加的数据块添加到Cache之后标记为最近访问。
- 如果Cache的容量已满,替换最久未访问的数据块为添加的数据块。
关于数据块的最近访问顺序可以表示为一个list,list中的元素按照访问顺序从最久到最近排序,当需要替换数据块的时候弹出list的首个元素,并将添加的数据块放到队尾;当需要访问数据块的时候,将访问的数据块放到队尾。
第一版代码
class LRUCache(object):
def __init__(self, capacity):
self.capacity = capacity
self._cache = []
self._cache_look_up = {}
def get(self, key):
if key not in self._cache_look_up:
return -1
self._cache.remove(key)
self._cache.append(key)
return self._cache_look_up[key]
def put(self, key, value):
if key in self._cache_look_up:
self._cache_look_up[key] = value
self._cache.remove(key)
self._cache.append(key)
return
else:
if len(self._cache) == self.capacity:
del_key = self._cache[0]
self._cache = self._cache[1:]
del self._cache_look_up[del_key]
self._cache.append(key)
self._cache_look_up[key] = value
这里使用了Python中内置的list数据结构作为保存访问顺序的队列,Python list实现为一块连续分配的内存(即C中的数组),因此如果list中删除元素或者插入元素的时间复杂度都为 Θ(n) ,但是根据索引访问元素或者在队尾插入元素都非常快,只需要常数时间。
除了保存访问顺序的队列以外,还需要保存key和value之间的对应关系,在这里直接使用Python dict来实现,相比C++中使用红黑树来实现的map,Python dict是通过hash table来实现的,因此搜索元素的时间复杂度能达到 Θ(1) 。在解决hash冲突的时候,Python dict使用了开放寻址法,通过二次探测函数计算下一个内存地址,当散列表中的装载因子达到2/3时,通过realloc函数重新分配内存空间。
Get( Θ(n) )
首先判断key是否在Cache之中存在,一种很常见的写法是:
if key not in self.cache_look_up.keys():
return -1
dict的keys方法返回了一个包含所有key的list,因此in操作的时间复杂度就变为 Θ(n) ,但是Python dict是通过hash table实现的,key的搜索可以通过 Θ(1) 来实现。即:
if key not self.cache_look_up:
return -1
参考链接:Check if a given key already exists in a dictionary(其中第二个回答)
接下来就是将最近访问的数据块放到队尾,这里使用了list数据结构,所以时间复杂度为 Θ(n) 。
Put( Θ(n) )
在设置新的数据块的时候,主要的耗时操作还是在list中删除或移动元素,时间复杂度也是为 Θ(n) 。
Result(TLE 17/18)
结果妥妥地TLE,毕竟题目要求的是两个操作都必须实现为 Θ(1) 。
第二版代码
优先队列除了使用传统的list来实现,还可以使用heap来实现,在heap中操作的时间复杂度一般为 log2N 。
在list中,直接可以通过key的位置的顺序表示访问顺序,但在堆中做不到,因此需要在堆的节点中存储一个访问时间,由于heapq库中没有提供针对节点接口的比较,因此节点自身需要重载比较运算符:
import time
class HeapNode(object):
def __init__(self, key, value):
self.key = key
self.value = value
self.access_time = time.time()
def update_time(self):
self.access_time = time.time()
def __lt__(self, other):
return self.access_time < other.access_time
def __ge__(self, other):
return self.access_time >= other.access_time
def __le__(self, other):
return self.access_time <= other.access_time
def __cmp__(self, other):
return self.access_time == other.access_time
实现堆的一些常用ADT:
class Heap(object):
def __init__(self):
self.heap = []
self.heap_size = 0
def insert(self, node):
self.heap_size += 1
heapq.heappush(self.heap, node)
def heapify(self):
self.heap.sort()
def pushpop(self, node):
return heapq.heappushpop(self.heap, node)
最后实现LRUCache:
class LRUCache(object):
def __init__(self, capacity):
self.capacity = capacity
self.cache = Heap()
self.cache_look_up = {}
def get(self, key):
if key not in self.cache_look_up:
return -1
heap_node = self.cache_look_up[key]
heap_node.update_time()
self.cache.heapify()
return heap_node.value
def put(self, key, value):
if key in self.cache_look_up:
heap_node = self.cache_look_up[key]
heap_node.value = value
heap_node.update_time()
self.cache.heapify()
else:
new_node = HeapNode(key, value)
self.cache_look_up[key] = new_node
if self.cache.heap_size == self.capacity:
min_node = self.cache.pushpop(new_node)
self.cache_look_up.pop(min_node.key)
return
self.cache.insert(new_node)
Get( Θ(log2N) )
获取key的时候直接查询dict即可,但是还需要在堆的优先队列中更新key对应的节点访问时间,在堆中维护一次堆性质的时间复杂度为 log2N 。
Put( Θ(log2N) )
在堆中弹出最小值,并将最小值替换成新插入的节点,维护一次堆性质,时间复杂度为 log2N 。
Result(TLE 16/18)
虽然堆实现的优先队列的时间复杂度比线性时间更少,但是在通过leetcode测试的时候明显比第一版代码更慢,因此有可能是堆的常数操作花费的时间更多。
第三版代码
在Python list中,即一段连续分配的内存中,移动和删除元素需要的时间复杂度都为 Θ(n) ,那么有没有办法实现为 Θ(1) 呢?与数组相对应的另一种线性结构就是链表,在移动和删除元素之间都只需要常数级的时间复杂度。
但是在链表中搜寻元素需要遍历链表,即 Θ(n) ,很明显如果不改进搜寻的方法,肯定也是吃一发TLE。因此,可以通过哈希表来记录key的节点地址,当需要访问指定节点的时候直接访问哈希表即可。
从哈希表中获取相对应的节点之后,还要对节点之间的移动,单向链表不能很好的满足需求,因此需要实现为双向链表。
class LinkNode(object):
def __init__(self, key, val, prev_node=None, next_node=None):
self.key = key
self.val = val
self.prev = prev_node
self.next = next_node
class DoubleLinkList(object):
def __init__(self, capacity):
self.capacity = capacity
self.size = 0
self.head = None
self.tail = None
def append(self, node):
"""
:param node:
:return:
append a node to the double link list last
"""
if self.size == self.capacity:
raise ValueError("The double link list has been full.")
self.size += 1
if self.head is None:
self.head = self.tail = node
return
self.tail.next = node
node.prev = self.tail
self.tail = node
def delete(self, node):
"""
:param node:
:return node:
delete a node in double link list. switch(node):
1.node == self.head
2.node == self.tail
3.node in the double link list middle
"""
if self.size == 0:
raise ValueError("can not delete empty double link list")
self.size -= 1
if node == self.head:
if node.next:
node.next.prev = None
self.head = node.next
elif node == self.tail:
if node.prev:
node.prev.next = None
self.tail = node.prev
else:
node.prev.next = node.next
node.next.prev = node.prev
return node
class LRUCache(object):
def __init__(self, capacity):
self.capacity = capacity
self.cache_look_up = {}
self.cache_list = DoubleLinkList(capacity)
def get(self, key):
if key not in self.cache_look_up:
return -1
node = self.cache_look_up[key]
self.cache_list.delete(node)
self.cache_list.append(node)
return node.val
def put(self, key, value):
if key in self.cache_look_up:
node = self.cache_look_up[key]
node.val = value
self.cache_list.delete(node)
self.cache_list.append(node)
else:
if self.capacity == self.cache_list.size:
head_node = self.cache_list.delete(self.cache_list.head)
del self.cache_look_up[head_node.key]
new_node = LinkNode(key, value)
self.cache_look_up[key] = new_node
self.cache_list.append(new_node)
Get( Θ(1) )
在Python dict中搜寻元素的时间复杂度为 Θ(1) ,而在双向链表中移动元素的时间复杂度也为 Θ(1) 。
Put( Θ(1) )
与Get的分析基本相同,操作基本可以在 Θ(1) 中实现。
Result(Accpeted)
第四版代码
也可以使用Collections中的OrderedDict来实现:
from collections import OrderedDict
class LRUCache(object):
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache:
return -1
value = self.cache[key]
self.cache.pop(key)
self.cache[key] = value
return value
def put(self, key, value):
if key in self.cache:
self.cache.pop(key)
self.cache[key] = value
else:
if len(self.cache.keys()) == self.capacity:
self.cache.popitem(last=False)
self.cache[key] = value
Result
如果还有更好的方法,欢迎跟我一起交流。