LRU算法是什么和为什么?
由于计算机缓存空间是有限的,所以要淘汰缓存中不常用的数据,留下常用的数据,腾出空间给新的数据。
LRU是Least recently used的缩写,字面意思是“最近最少使用”,也可理解为“最久未使用”,最久未使用的当然就要被淘汰了。
思路一
使用有序单链表实现。
规定:越靠近链表尾部的结点是越早之前访问的,越靠近链表头部的节点是最近访问的。
当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。
1. 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
2. 如果此数据没有在缓存链表中,又可以分为两种情况:
如果此时缓存未满,则将此结点直接插入到链表的头部;
如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
时间复杂度
在缓存中查找某个数据,不管缓存有没有满,都需要遍历一遍链表,所以缓存访问的时间复杂度为 O(n)。
访问一个新数据时,先要查找缓存,再将数据放入缓存,由于查找缓存的原因,所以更新缓存的复杂度为O(n)。
思路二
使用哈希表+链表实现
因为哈希表的查找是很快的,时间复杂度为O(1)。可以将查找和更新缓存的时间复杂度都降为O(1)。
哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。
借助哈希表赋予了链表快速查找的特性:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。
为什么要是双向链表,单链表行不行?
因为删除一个链表节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。
既然哈希表中已经存了 key,为什么链表中还要存键值对呢,只存值不就行了?
当你删除一个链表节点,同时还要把哈希表中映射到该节点的 key 删除,而这个 key 只能从链表节点得到。如果链表节点结构中只存储 val,那么就无法得知 key 是什么,就无法删除哈希表中的key,造成错误。
使用有序字典来实现LRU算法
Python有个collections.OrderedDict类,是一个有序字典,这样就相当于实现了哈希表+双向链表。
即有O(1)的查找速度,又有O(1)的更新速度。
这样就省去了实现哈希表+双向链表的代码。
Python代码实现
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity # 缓存容量
self.cache = collections.OrderedDict() # 缓存,有序字典,代替散列表+链表
def get(self, key):
if key not in self.cache:
return -1 # 访问的数据不在缓存中则返回-1
value = self.cache.pop(key) # 将命中缓存的数据移除
self.cache[key] = value # 将命中缓存的数据放到头部
print("get操作后的缓存: ", self.cache)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.pop(key) # 已经在缓存中,则先移除老的数据
elif len(self.cache) == self.capacity:
self.cache.popitem(last=False) # 不在缓存中并且到达最大容量,则把最后的数据淘汰
self.cache[key] = value # 将新数据添加到头部
print("put操作后的缓存: ", self.cache)
if __name__ == "__main__":
test = LRUCache(5)
test.put('a', 1)
test.put('b', 2)
test.put('c', 3)
test.put('d', 4)
test.put('e', 5)
print(test.get('a'))
print(test.get('c'))
print(test.get('d'))
print(test.get('e'))
print(test.get('a'))
test.put('f', 6)
print(test.get('b'))