Title

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) -:如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 该操作会使得密钥 2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得密钥 1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

Solve

自带数据结构:

这尼玛好熟悉啊,Python里不是有一种结合了哈希表和双链表的数据结构叫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=key)
        return self[key]

    def put(self, key: int, value: int) -> None:
        if key in self:
            self.move_to_end(key=key)
        self[key] = value
        if len(self) > self.capacity:
            self.popitem(last=False)

但显然在面试的时候这不是面试官想要的结果,因此我们还是用哈希表+双链表维护一个数据结构吧。


哈希表+双链表:

LRU 缓存机制可以通过哈希表辅以双链表维护所有在缓存中的键值对。

  • 双链表按照被使用的顺序存储键值对,靠近头部的键值对是最近被使用的,靠近尾部的键值对是最久没被使用过的。
  • 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双链表中的位置。

这样以来,首先使用哈希表进行定位,找出缓存项在双链表中的位置,随后将其移动到双链表的头部,即可在O(1)的时间内完成get或者put操作。

具体的方法如下:

  • 对于get操作,首先判断key是否存在:

    • 如果key不存在,返回-1;
    • 如果key存在,则key对应的节点是最近被使用的节点,通过哈希表定位到该节点在双链表中的位置并将其移动到双链表的头部,最后返回该节点的值。
  • 对于post操作,首先判断key是否存在:

    • 如果key不存在,使用keyvalue创建一个新的节点,在双链表的头部添加该节点,并将key和该节点添加到哈希表中,然后判断双链表的节点数是否超出容量:
      • 如果超出容量,删除双链表的尾部节点,并删除哈希表中对应的项
    • 如果key存在,则与get操作类似,先通过哈希表定位,再将对应的节点的值更新为value,并将该节点移动到双链表的头部。

上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1) 时间内完成。

小贴士

在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。

复杂度分析

时间复杂度:对于 put 和 get 都是 O(1)。

空间复杂度:O(capacity),因为哈希表和双向链表最多存储 capacity+1 个元素。

Code

class DoubleLinkNode:
	def __init__(self, key=0, value=0):
		self.key, self.value, self.prev, self.next = key, value, None, None


class LRUCache:

	def __init__(self, capacity: int):
		self.cache, self.capacity, self.size = dict(), capacity, 0
		self.head, self.tail = DoubleLinkNode(), DoubleLinkNode()
		self.head.next, self.tail.prev = self.tail, self.head

	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 = DoubleLinkNode(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